From 5b315858a3f1c07557c31c37e7f3f0f53e34f971 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 20 Jan 2021 10:07:13 -0600 Subject: [PATCH 01/28] Instances latency distribution chart (#88546) * Instances latency distribution chart Create an instances row component that does the data fetching for the instances table and the distribution chart. Use the same data for both the chart and the table. Tooltips and selection are disabled on the chart. * import fix * rename ServiceOverviewInstancesRow to ServiceOverviewInstancesChartAndTable * Updates based on feedback * remove stuff * hasdata --- .../components/app/service_overview/index.tsx | 15 ++- ...ice_overview_instances_chart_and_table.tsx | 75 +++++++++++++ .../index.tsx | 74 +++++-------- .../index.tsx | 101 ++++++++++++++++++ 4 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index f7720589359c8..c6cc59876fe35 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -19,7 +19,7 @@ import { SearchBar } from '../../shared/search_bar'; import { UserExperienceCallout } from '../transaction_overview/user_experience_callout'; import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; -import { ServiceOverviewInstancesTable } from './service_overview_instances_table'; +import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; import { useShouldUseMobileLayout } from './use_should_use_mobile_layout'; @@ -131,9 +131,16 @@ export function ServiceOverview({ {!isRumAgent && ( - - - + + + )} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx new file mode 100644 index 0000000000000..f7c2891bb3e65 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart'; +import { ServiceOverviewInstancesTable } from './service_overview_instances_table'; + +interface ServiceOverviewInstancesChartAndTableProps { + chartHeight: number; + serviceName: string; +} + +export function ServiceOverviewInstancesChartAndTable({ + chartHeight, + serviceName, +}: ServiceOverviewInstancesChartAndTableProps) { + const { transactionType } = useApmServiceContext(); + + const { + urlParams: { start, end }, + uiFilters, + } = useUrlParams(); + + const { data = [], status } = useFetcher(() => { + if (!start || !end || !transactionType) { + return; + } + + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/service_overview_instances', + params: { + path: { + serviceName, + }, + query: { + start, + end, + transactionType, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + }, + }, + }); + }, [start, end, serviceName, transactionType, uiFilters]); + + return ( + <> + + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 1d0e1e50c1489..8d84ad7878ec7 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -4,52 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem } from '@elastic/eui'; -import { EuiInMemoryTable } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; -import { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; import { isJavaAgentName } from '../../../../../common/agent_name'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { asMillisecondDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { - APIReturnType, - callApmApi, -} from '../../../../services/rest/createCallApmApi'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; -import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { px, unit } from '../../../../style/variables'; -import { ServiceOverviewTableContainer } from '../service_overview_table_container'; -import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink'; +import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { ServiceOverviewTableContainer } from '../service_overview_table_container'; type ServiceInstanceItem = ValuesType< APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances'> >; interface Props { + items?: ServiceInstanceItem[]; serviceName: string; + status: FETCH_STATUS; } -export function ServiceOverviewInstancesTable({ serviceName }: Props) { - const { agentName, transactionType } = useApmServiceContext(); - - const { - urlParams: { start, end }, - uiFilters, - } = useUrlParams(); +export function ServiceOverviewInstancesTable({ + items = [], + serviceName, + status, +}: Props) { + const { agentName } = useApmServiceContext(); const columns: Array> = [ { @@ -197,31 +196,8 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { }, ]; - const { data = [], status } = useFetcher(() => { - if (!start || !end || !transactionType) { - return; - } - - return callApmApi({ - endpoint: - 'GET /api/apm/services/{serviceName}/service_overview_instances', - params: { - path: { - serviceName, - }, - query: { - start, - end, - transactionType, - uiFilters: JSON.stringify(uiFilters), - numBuckets: 20, - }, - }, - }); - }, [start, end, serviceName, transactionType, uiFilters]); - // need top-level sortable fields for the managed table - const items = data.map((item) => ({ + const tableItems = items.map((item) => ({ ...item, latencyValue: item.latency?.value ?? 0, throughputValue: item.throughput?.value ?? 0, @@ -250,7 +226,7 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { > ; + status: FETCH_STATUS; +} + +export function InstancesLatencyDistributionChart({ + height, + items = [], + status, +}: InstancesLatencyDistributionChartProps) { + const hasData = items.length > 0; + + const theme = useTheme(); + const chartTheme = { + ...useChartTheme(), + bubbleSeriesStyle: { + point: { + strokeWidth: 0, + fill: theme.eui.euiColorVis1, + radius: 4, + }, + }, + }; + + const maxLatency = Math.max(...items.map((item) => item.latency?.value ?? 0)); + const latencyFormatter = getDurationFormatter(maxLatency); + + return ( + + +

+ {i18n.translate('xpack.apm.instancesLatencyDistributionChartTitle', { + defaultMessage: 'Instances latency distribution', + })} +

+
+ + + + item.throughput?.value} + xScaleType={ScaleType.Linear} + yAccessors={[(item) => item.latency?.value]} + yScaleType={ScaleType.Linear} + /> + + + + +
+ ); +} From 0cd8c90a7cc9e98744acf25acfd3acc174386b88 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 20 Jan 2021 17:11:37 +0100 Subject: [PATCH 02/28] [Lens] (Accessibility) Added button to execute drag and drop to workspace (#85960) --- .../editor_frame/data_panel_wrapper.tsx | 6 +- .../editor_frame/editor_frame.test.tsx | 9 +- .../editor_frame/editor_frame.tsx | 59 +++++- .../editor_frame/suggestion_helpers.test.ts | 133 +++++++++++- .../editor_frame/suggestion_helpers.ts | 34 ++++ .../workspace_panel/workspace_panel.test.tsx | 189 +++--------------- .../workspace_panel/workspace_panel.tsx | 41 +--- .../datapanel.test.tsx | 2 + .../indexpattern_datasource/datapanel.tsx | 8 + .../indexpattern_datasource/field_item.scss | 3 +- .../field_item.test.tsx | 2 + .../indexpattern_datasource/field_item.tsx | 169 +++++++++++++--- .../indexpattern_datasource/field_list.tsx | 9 + .../fields_accordion.test.tsx | 2 + .../fields_accordion.tsx | 8 +- x-pack/plugins/lens/public/types.ts | 4 +- 16 files changed, 447 insertions(+), 231 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index d00357058bb57..69bdff0151f6c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; -import { DragContext } from '../../drag_drop'; +import { DragContext, Dragging } from '../../drag_drop'; import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; import { Query, Filter } from '../../../../../../src/plugins/data/public'; @@ -26,6 +26,8 @@ interface DataPanelWrapperProps { query: Query; dateRange: FramePublicAPI['dateRange']; filters: Filter[]; + dropOntoWorkspace: (field: Dragging) => void; + hasSuggestionForField: (field: Dragging) => boolean; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -51,6 +53,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dateRange: props.dateRange, filters: props.filters, showNoDataPopover: props.showNoDataPopover, + dropOntoWorkspace: props.dropOntoWorkspace, + hasSuggestionForField: props.hasSuggestionForField, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index ef95314c55581..76394c2901aaa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -632,16 +632,19 @@ describe('editor_frame', () => { ); }); + const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] + .setState; + + mockDatasource.renderDataPanel.mockClear(); + const updatedState = { title: 'shazm', }; - const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] - .setState; act(() => { setDatasourceState(updatedState); }); - expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(2); + expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(1); expect(mockDatasource.renderDataPanel).toHaveBeenLastCalledWith( expect.any(Element), expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index d872920d815ad..2cb815596d8b9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -16,14 +16,19 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; -import { RootDragDropProvider } from '../../drag_drop'; +import { Dragging, RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; import { EditorFrameStartPlugins } from '../service'; import { initializeDatasources, createDatasourceLayers } from './state_helpers'; -import { applyVisualizeFieldSuggestions } from './suggestion_helpers'; +import { + applyVisualizeFieldSuggestions, + getTopSuggestionForField, + switchToSuggestion, +} from './suggestion_helpers'; +import { trackUiEvent } from '../../lens_ui_telemetry'; export interface EditorFrameProps { doc?: Document; @@ -254,6 +259,53 @@ export function EditorFrame(props: EditorFrameProps) { ] ); + const getSuggestionForField = React.useCallback( + (field: Dragging) => { + const { activeDatasourceId, datasourceStates } = state; + const activeVisualizationId = state.visualization.activeId; + const visualizationState = state.visualization.state; + const { visualizationMap, datasourceMap } = props; + + if (!field || !activeDatasourceId) { + return; + } + + return getTopSuggestionForField( + datasourceLayers, + activeVisualizationId, + visualizationMap, + visualizationState, + datasourceMap[activeDatasourceId], + datasourceStates, + field + ); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + state.visualization.state, + props.datasourceMap, + props.visualizationMap, + state.activeDatasourceId, + state.datasourceStates, + ] + ); + + const hasSuggestionForField = React.useCallback( + (field: Dragging) => getSuggestionForField(field) !== undefined, + [getSuggestionForField] + ); + + const dropOntoWorkspace = React.useCallback( + (field) => { + const suggestion = getSuggestionForField(field); + if (suggestion) { + trackUiEvent('drop_onto_workspace'); + switchToSuggestion(dispatch, suggestion, 'SWITCH_VISUALIZATION'); + } + }, + [getSuggestionForField] + ); + return ( } configPanel={ @@ -310,6 +364,7 @@ export function EditorFrame(props: EditorFrameProps) { core={props.core} plugins={props.plugins} visualizeTriggerFieldContext={visualizeTriggerFieldContext} + getSuggestionForField={getSuggestionForField} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index c2534c8337df0..745a2250a1deb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSuggestions } from './suggestion_helpers'; +import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers'; import { createMockVisualization, createMockDatasource, DatasourceMock } from '../mocks'; -import { TableSuggestion, DatasourceSuggestion } from '../../types'; +import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types'; import { PaletteOutput } from 'src/plugins/charts/public'; const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({ @@ -472,4 +472,133 @@ describe('suggestion helpers', () => { }) ); }); + + describe('getTopSuggestionForField', () => { + let mockVisualization1: jest.Mocked; + let mockVisualization2: jest.Mocked; + let mockDatasourceState: unknown; + let defaultParams: Parameters; + beforeEach(() => { + datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([ + { + state: {}, + table: { + isMultiRow: true, + layerId: '1', + columns: [], + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + mockVisualization1 = createMockVisualization(); + mockVisualization1.getSuggestions.mockReturnValue([ + { + score: 0.3, + title: 'second suggestion', + state: { second: true }, + previewIcon: 'empty', + }, + { + score: 0.5, + title: 'top suggestion', + state: { first: true }, + previewIcon: 'empty', + }, + ]); + mockVisualization2 = createMockVisualization(); + mockVisualization2.getSuggestions.mockReturnValue([ + { + score: 0.8, + title: 'other vis suggestion', + state: {}, + previewIcon: 'empty', + }, + ]); + mockDatasourceState = { myDatasourceState: true }; + defaultParams = [ + { + '1': { + getTableSpec: () => [{ columnId: 'col1' }], + datasourceId: '', + getOperationForColumnId: jest.fn(), + }, + }, + 'vis1', + { vis1: mockVisualization1 }, + {}, + datasourceMap.mock, + { + mockindexpattern: { state: mockDatasourceState, isLoading: false }, + }, + { id: 'myfield' }, + ]; + }); + + it('should return top suggestion for field', () => { + const result = getTopSuggestionForField(...defaultParams); + expect(result!.title).toEqual('top suggestion'); + expect(datasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( + mockDatasourceState, + { + id: 'myfield', + } + ); + }); + + it('should return nothing if visualization does not produce suggestions', () => { + mockVisualization1.getSuggestions.mockReturnValue([]); + const result = getTopSuggestionForField(...defaultParams); + expect(result).toEqual(undefined); + }); + + it('should return nothing if datasource does not produce suggestions', () => { + datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]); + defaultParams[2] = { + vis1: { ...mockVisualization1, getSuggestions: () => [] }, + vis2: mockVisualization2, + }; + const result = getTopSuggestionForField(...defaultParams); + expect(result).toEqual(undefined); + }); + + it('should not consider suggestion from other visualization if there is data', () => { + defaultParams[2] = { + vis1: { ...mockVisualization1, getSuggestions: () => [] }, + vis2: mockVisualization2, + }; + const result = getTopSuggestionForField(...defaultParams); + expect(result).toBeUndefined(); + }); + + it('should consider top suggestion from other visualization if there is no data', () => { + const mockVisualization3 = createMockVisualization(); + defaultParams[0] = { + '1': { + getTableSpec: () => [], + datasourceId: '', + getOperationForColumnId: jest.fn(), + }, + }; + mockVisualization1.getSuggestions.mockReturnValue([]); + mockVisualization3.getSuggestions.mockReturnValue([ + { + score: 0.1, + title: 'low ranking suggestion', + state: {}, + previewIcon: 'empty', + }, + ]); + defaultParams[2] = { + vis1: mockVisualization1, + vis2: mockVisualization2, + vis3: mockVisualization3, + }; + const result = getTopSuggestionForField(...defaultParams); + expect(result!.title).toEqual('other vis suggestion'); + expect(mockVisualization1.getSuggestions).toHaveBeenCalled(); + expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); + expect(mockVisualization3.getSuggestions).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index daaf893f2a703..5cdc5ce592497 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -16,8 +16,10 @@ import { TableChangeType, TableSuggestion, DatasourceSuggestion, + DatasourcePublicAPI, } from '../../types'; import { Action } from './state_management'; +import { Dragging } from '../../drag_drop'; export interface Suggestion { visualizationId: string; @@ -221,3 +223,35 @@ export function switchToSuggestion( dispatch(action); } + +export function getTopSuggestionForField( + datasourceLayers: Record, + activeVisualizationId: string | null, + visualizationMap: Record>, + visualizationState: unknown, + datasource: Datasource, + datasourceStates: Record, + field: Dragging +) { + const hasData = Object.values(datasourceLayers).some( + (datasourceLayer) => datasourceLayer.getTableSpec().length > 0 + ); + + const mainPalette = + activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette + ? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState) + : undefined; + const suggestions = getSuggestions({ + datasourceMap: { [datasource.id]: datasource }, + datasourceStates, + visualizationMap: + hasData && activeVisualizationId + ? { [activeVisualizationId]: visualizationMap[activeVisualizationId] } + : visualizationMap, + activeVisualizationId, + visualizationState, + field, + mainPalette, + }); + return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 6411b0e5f1ad9..ddb2640d50d59 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; -import { FramePublicAPI, TableSuggestion, Visualization } from '../../../types'; +import { FramePublicAPI, Visualization } from '../../../types'; import { createMockVisualization, createMockDatasource, @@ -85,6 +85,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -108,6 +109,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -131,6 +133,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -168,6 +171,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -241,6 +245,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -284,6 +289,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -335,6 +341,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -415,6 +422,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); }); @@ -471,6 +479,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); }); @@ -528,6 +537,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -569,6 +579,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -612,6 +623,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -652,6 +664,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); @@ -690,6 +703,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); }); @@ -734,6 +748,7 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={() => undefined} /> ); }); @@ -756,6 +771,7 @@ describe('workspace_panel', () => { describe('suggestions from dropping in workspace panel', () => { let mockDispatch: jest.Mock; + let mockGetSuggestionForField: jest.Mock; let frame: jest.Mocked; const draggedField = { id: 'field' }; @@ -763,6 +779,7 @@ describe('workspace_panel', () => { beforeEach(() => { frame = createMockFramePublicAPI(); mockDispatch = jest.fn(); + mockGetSuggestionForField = jest.fn(); }); function initComponent(draggingContext = draggedField) { @@ -790,43 +807,23 @@ describe('workspace_panel', () => { ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} plugins={{ uiActions: uiActionsMock, data: dataMock }} + getSuggestionForField={mockGetSuggestionForField} /> ); } it('should immediately transition if exactly one suggestion is returned', () => { - const expectedTable: TableSuggestion = { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }; - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: expectedTable, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); + mockGetSuggestionForField.mockReturnValue({ + visualizationId: 'vis', + datasourceState: {}, + datasourceId: 'mock', + visualizationState: {}, + }); initComponent(); instance.find(DragDrop).prop('onDrop')!(draggedField); - expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1); - expect(mockVisualization.getSuggestions).toHaveBeenCalledWith( - expect.objectContaining({ - table: expectedTable, - }) - ); expect(mockDispatch).toHaveBeenCalledWith({ type: 'SWITCH_VISUALIZATION', newVisualizationId: 'vis', @@ -837,80 +834,12 @@ describe('workspace_panel', () => { }); it('should allow to drop if there are suggestions', () => { - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); - }); - - it('should refuse to drop if there only suggestions from other visualizations if there are data tables', () => { - frame.datasourceLayers.a = mockDatasource.publicAPIMock; - mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization2.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); - }); - - it('should allow to drop if there are suggestions from active visualization even if there are data tables', () => { - frame.datasourceLayers.a = mockDatasource.publicAPIMock; - mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); + mockGetSuggestionForField.mockReturnValue({ + visualizationId: 'vis', + datasourceState: {}, + datasourceId: 'mock', + visualizationState: {}, + }); initComponent(); expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); }); @@ -919,61 +848,5 @@ describe('workspace_panel', () => { initComponent(); expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); }); - - it('should immediately transition to the first suggestion if there are multiple', () => { - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - columns: [], - layerId: '1', - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - { - state: {}, - table: { - isMultiRow: true, - columns: [], - layerId: '1', - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'second suggestion', - state: {}, - previewIcon: 'empty', - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.8, - title: 'first suggestion', - state: { - isFirst: true, - }, - previewIcon: 'empty', - }, - ]); - - initComponent(); - instance.find(DragDrop).prop('onDrop')!(draggedField); - - expect(mockDispatch).toHaveBeenCalledWith({ - type: 'SWITCH_VISUALIZATION', - newVisualizationId: 'vis', - initialState: { - isFirst: true, - }, - datasourceState: {}, - datasourceId: 'mock', - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index eb16dabfd2f90..5fc7b80a3d0ce 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -39,8 +39,8 @@ import { isLensFilterEvent, isLensEditEvent, } from '../../../types'; -import { DragDrop, DragContext } from '../../../drag_drop'; -import { getSuggestions, switchToSuggestion } from '../suggestion_helpers'; +import { DragDrop, DragContext, Dragging } from '../../../drag_drop'; +import { Suggestion, switchToSuggestion } from '../suggestion_helpers'; import { buildExpression } from '../expression_helpers'; import { debouncedComponent } from '../../../debounced_component'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -75,6 +75,7 @@ export interface WorkspacePanelProps { plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; title?: string; visualizeTriggerFieldContext?: VisualizeFieldContext; + getSuggestionForField: (field: Dragging) => Suggestion | undefined; } interface WorkspaceState { @@ -97,43 +98,11 @@ export function WorkspacePanel({ ExpressionRenderer: ExpressionRendererComponent, title, visualizeTriggerFieldContext, + getSuggestionForField, }: WorkspacePanelProps) { const dragDropContext = useContext(DragContext); - const suggestionForDraggedField = useMemo( - () => { - if (!dragDropContext.dragging || !activeDatasourceId) { - return; - } - - const hasData = Object.values(framePublicAPI.datasourceLayers).some( - (datasource) => datasource.getTableSpec().length > 0 - ); - - const mainPalette = - activeVisualizationId && - visualizationMap[activeVisualizationId] && - visualizationMap[activeVisualizationId].getMainPalette - ? visualizationMap[activeVisualizationId].getMainPalette!(visualizationState) - : undefined; - const suggestions = getSuggestions({ - datasourceMap: { [activeDatasourceId]: datasourceMap[activeDatasourceId] }, - datasourceStates, - visualizationMap: - hasData && activeVisualizationId - ? { [activeVisualizationId]: visualizationMap[activeVisualizationId] } - : visualizationMap, - activeVisualizationId, - visualizationState, - field: dragDropContext.dragging, - mainPalette, - }); - - return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dragDropContext.dragging] - ); + const suggestionForDraggedField = getSuggestionForField(dragDropContext.dragging); const [localState, setLocalState] = useState({ expressionBuildError: undefined, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 3d55494fd260c..8e41abf23e934 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -260,6 +260,8 @@ describe('IndexPattern Data Panel', () => { query: { query: '', language: 'lucene' }, filters: [], showNoDataPopover: jest.fn(), + dropOntoWorkspace: jest.fn(), + hasSuggestionForField: jest.fn(() => false), }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 4bb18d1ee4a17..63f6f77dd10cc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -100,6 +100,8 @@ export function IndexPatternDataPanel({ changeIndexPattern, charts, showNoDataPopover, + dropOntoWorkspace, + hasSuggestionForField, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( @@ -193,6 +195,8 @@ export function IndexPatternDataPanel({ onChangeIndexPattern={onChangeIndexPattern} existingFields={state.existingFields} existenceFetchFailed={state.existenceFetchFailed} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} /> )} @@ -241,6 +245,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ data, existingFields, charts, + dropOntoWorkspace, + hasSuggestionForField, }: Omit & { data: DataPublicPluginStart; currentIndexPatternId: string; @@ -593,6 +599,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ currentIndexPatternId={currentIndexPatternId} existenceFetchFailed={existenceFetchFailed} existFieldsInIndex={!!allFields.length} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss index 1b55d9623e223..8c10ca9d30b73 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss @@ -42,7 +42,6 @@ max-width: 300px; } -.lnsFieldItem__buttonGroup { - // Enforce lowercase for buttons or else some browsers inherit all caps from flyout title +.lnsFieldItem__fieldPanelTitle { text-transform: none; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 2f3549911dfe7..1019b2c33e0e5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -95,6 +95,8 @@ describe('IndexPattern Field Item', () => { }, exists: true, chartsThemeService, + dropOntoWorkspace: () => {}, + hasSuggestionForField: () => false, }; data.fieldFormats = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 0e602893beae6..740b557b668b7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -6,10 +6,11 @@ import './field_item.scss'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import DateMath from '@elastic/datemath'; import { EuiButtonGroup, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIconTip, @@ -18,7 +19,9 @@ import { EuiPopoverFooter, EuiPopoverTitle, EuiProgress, + EuiSpacer, EuiText, + EuiTitle, EuiToolTip, } from '@elastic/eui'; import { @@ -45,7 +48,7 @@ import { import { FieldButton } from '../../../../../src/plugins/kibana_react/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DraggedField } from './indexpattern'; -import { DragDrop } from '../drag_drop'; +import { DragDrop, Dragging } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; @@ -66,6 +69,8 @@ export interface FieldItemProps { chartsThemeService: ChartsPluginSetup['theme']; filters: Filter[]; hideDetails?: boolean; + dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; + hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; } interface State { @@ -95,10 +100,19 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { dateRange, filters, hideDetails, + dropOntoWorkspace, } = props; const [infoIsOpen, setOpen] = useState(false); + const dropOntoWorkspaceAndClose = useCallback( + (droppedField: Dragging) => { + dropOntoWorkspace(droppedField); + setOpen(false); + }, + [dropOntoWorkspace, setOpen] + ); + const [state, setState] = useState({ isLoading: false, }); @@ -142,10 +156,6 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { } function togglePopover() { - if (hideDetails) { - return; - } - setOpen(!infoIsOpen); if (!infoIsOpen) { trackUiEvent('indexpattern_field_info_click'); @@ -227,8 +237,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { closePopover={() => setOpen(false)} anchorPosition="rightUp" panelClassName="lnsFieldItem__fieldPanel" + initialFocus=".lnsFieldItem__fieldPanel" > - + ); @@ -236,6 +251,40 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { export const FieldItem = debouncedComponent(InnerFieldItem); +function FieldPanelHeader({ + indexPatternId, + field, + hasSuggestionForField, + dropOntoWorkspace, +}: { + field: IndexPatternField; + indexPatternId: string; + hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; + dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; +}) { + const draggableField = { + indexPatternId, + id: field.name, + field, + }; + + return ( + + + +
{field.displayName}
+
+
+ + +
+ ); +} + function FieldItemPopoverContents(props: State & FieldItemProps) { const { histogram, @@ -247,6 +296,9 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { sampledValues, chartsThemeService, data: { fieldFormats }, + dropOntoWorkspace, + hasSuggestionForField, + hideDetails, } = props; const chartTheme = chartsThemeService.useChartsTheme(); @@ -270,6 +322,19 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { const [showingHistogram, setShowingHistogram] = useState(histogramDefault); + const panelHeader = ( + + ); + + if (hideDetails) { + return panelHeader; + } + let formatter: { convert: (data: unknown) => string }; if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) { const FormatType = fieldFormats.getType(indexPattern.fieldFormatMap[field.name].id); @@ -300,12 +365,16 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { (!props.topValues || props.topValues.buckets.length === 0) ) { return ( - - {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: - 'This field is empty because it doesn’t exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.', - })} - + <> + {panelHeader} + + + {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: + 'This field is empty because it doesn’t exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.', + })} + + ); } @@ -340,31 +409,40 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { ); } else if (field.type === 'date') { title = ( - <> - {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { - defaultMessage: 'Time distribution', - })} - + +
+ {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { + defaultMessage: 'Time distribution', + })} +
+
); } else if (topValues && topValues.buckets.length) { title = ( - <> - {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { - defaultMessage: 'Top values', - })} - + +
+ {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { + defaultMessage: 'Top values', + })} +
+
); } function wrapInPopover(el: React.ReactElement) { return ( <> - {title ? {title} : <>} + {panelHeader} + + {title ? title : <>} + + + {el} {props.totalDocuments ? ( - + {props.sampledDocuments && ( <> {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { @@ -552,3 +630,44 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { } return <>; } + +const DragToWorkspaceButton = ({ + field, + dropOntoWorkspace, + isEnabled, +}: { + field: { + indexPatternId: string; + id: string; + field: IndexPatternField; + }; + dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; + isEnabled: boolean; +}) => { + const buttonTitle = isEnabled + ? i18n.translate('xpack.lens.indexPattern.moveToWorkspace', { + defaultMessage: 'Add {field} to workspace', + values: { + field: field.field.name, + }, + }) + : i18n.translate('xpack.lens.indexPattern.moveToWorkspaceDisabled', { + defaultMessage: + "This field can't be added to the workspace automatically. You can still use it directly in the configuration panel.", + }); + + return ( + + + { + dropOntoWorkspace(field); + }} + /> + + + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index 5668e85510f9d..7bcd44e3d25d0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -12,6 +12,7 @@ import { FieldItem } from './field_item'; import { NoFieldsCallout } from './no_fields_callout'; import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; +import { DatasourceDataPanelProps } from '../types'; const PAGINATION_SIZE = 50; export type FieldGroups = Record< @@ -48,6 +49,8 @@ export function FieldList({ filter, currentIndexPatternId, existFieldsInIndex, + dropOntoWorkspace, + hasSuggestionForField, }: { exists: (field: IndexPatternField) => boolean; fieldGroups: FieldGroups; @@ -60,6 +63,8 @@ export function FieldList({ }; currentIndexPatternId: string; existFieldsInIndex: boolean; + dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; + hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; }) { const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); @@ -137,6 +142,8 @@ export function FieldList({ field={field} hideDetails={true} key={field.name} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} /> )) )} @@ -147,6 +154,8 @@ export function FieldList({ .map(([key, fieldGroup]) => ( { fieldProps, renderCallout:
Callout
, exists: () => true, + dropOntoWorkspace: () => {}, + hasSuggestionForField: () => false, }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index ef249f87f05e4..11adf1a128c1b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -50,6 +50,8 @@ export interface FieldsAccordionProps { exists: (field: IndexPatternField) => boolean; showExistenceFetchError?: boolean; hideDetails?: boolean; + dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; + hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; } export const InnerFieldsAccordion = function InnerFieldsAccordion({ @@ -67,6 +69,8 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ exists, hideDetails, showExistenceFetchError, + dropOntoWorkspace, + hasSuggestionForField, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField) => ( @@ -76,9 +80,11 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ field={field} exists={exists(field)} hideDetails={hideDetails} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} /> ), - [fieldProps, exists, hideDetails] + [fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField] ); const titleClassname = classNames({ diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index fe35e07081bf3..bba601f942380 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -16,7 +16,7 @@ import { Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; -import { DragContextState } from './drag_drop'; +import { DragContextState, Dragging } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; @@ -217,6 +217,8 @@ export interface DatasourceDataPanelProps { query: Query; dateRange: DateRange; filters: Filter[]; + dropOntoWorkspace: (field: Dragging) => void; + hasSuggestionForField: (field: Dragging) => boolean; } interface SharedDimensionProps { From 48cb37945a990fd062b61864f416ddb39aa02290 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 20 Jan 2021 17:24:31 +0100 Subject: [PATCH 03/28] [UX] fix impacted page load errors (#88597) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/queries.test.ts.snap | 14 + .../server/lib/rum_client/get_js_errors.ts | 20 +- .../es_archiver/rum_test_data/data.json.gz | Bin 0 -> 836153 bytes .../es_archiver/rum_test_data/mappings.json | 9145 +++++++++++++++++ .../trial/tests/csm/js_errors.ts | 40 +- 5 files changed, 9210 insertions(+), 9 deletions(-) create mode 100644 x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/data.json.gz create mode 100644 x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/mappings.json diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index b89c46f6e3fc5..6b4bc844f21c3 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -89,6 +89,20 @@ Object { "size": 5, }, }, + "impactedPages": Object { + "aggs": Object { + "pageCount": Object { + "cardinality": Object { + "field": "transaction.id", + }, + }, + }, + "filter": Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + }, "sample": Object { "top_hits": Object { "_source": Array [ diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts index bbceef418ea45..229a2ef63e481 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts @@ -12,7 +12,9 @@ import { ERROR_EXC_TYPE, ERROR_GROUP_ID, TRANSACTION_ID, + TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; export async function getJSErrors({ setup, @@ -57,6 +59,20 @@ export async function getJSErrors({ from: pageIndex * pageSize, }, }, + impactedPages: { + filter: { + term: { + [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD, + }, + }, + aggs: { + pageCount: { + cardinality: { + field: TRANSACTION_ID, + }, + }, + }, + }, sample: { top_hits: { _source: [ @@ -86,9 +102,9 @@ export async function getJSErrors({ totalErrorPages: totalErrorPages?.value ?? 0, totalErrors: response.hits.total.value ?? 0, totalErrorGroups: totalErrorGroups?.value ?? 0, - items: errors?.buckets.map(({ sample, doc_count: count, key }) => { + items: errors?.buckets.map(({ sample, key, impactedPages }) => { return { - count, + count: impactedPages.pageCount.value, errorGroupId: key, errorMessage: (sample.hits.hits[0]._source as { error: { exception: Array<{ message: string }> }; diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..92e8e04f63af6549ea2300f5c9ceb96f038281b8 GIT binary patch literal 836153 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�L9}u{K~xhO303_~7wSYi5>eGgETAC* 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

GQC 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

)4{=y|Lu(@s@Ek7$-hu=Y$+ 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

-af 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/mappings.json new file mode 100644 index 0000000000000..3966692d5a4fc --- /dev/null +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data/mappings.json @@ -0,0 +1,9145 @@ +{ + "type": "index", + "value": { + "index": "apm-8.0.0-error-2020.12.03-000005", + "mappings": { + "_meta": { + "beat": "apm", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "transaction.marks": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "transaction.marks.*" + } + }, + { + "transaction.marks.*.*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "transaction.marks.*.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "dynamic": "false", + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "path": "agent.name", + "type": "alias" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "child": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "dynamic": "false", + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "dynamic": "false", + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "culprit": { + "ignore_above": 1024, + "type": "keyword" + }, + "exception": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handled": { + "type": "boolean" + }, + "message": { + "norms": false, + "type": "text" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "grouping_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "param_message": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "experimental": { + "dynamic": "true", + "type": "object" + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "dynamic": "false", + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "dynamic": "false", + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "finished": { + "type": "boolean" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "dynamic": "false", + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "dynamic": "true", + "properties": { + "city": { + "type": "keyword" + }, + "company": { + "type": "keyword" + }, + "country_code": { + "type": "keyword" + }, + "customer_tier": { + "type": "keyword" + }, + "foo": { + "type": "keyword" + }, + "git_rev": { + "type": "keyword" + }, + "in_eu": { + "type": "boolean" + }, + "ip": { + "type": "keyword" + }, + "lang": { + "type": "keyword" + }, + "lorem": { + "type": "keyword" + }, + "multi-line": { + "type": "keyword" + }, + "request_id": { + "type": "keyword" + }, + "this-is-a-very-long-tag-name-without-any-spaces": { + "type": "keyword" + }, + "u": { + "type": "keyword" + } + } + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "metricset": { + "properties": { + "period": { + "type": "long" + } + } + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "dynamic": "false", + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "listening": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_major": { + "type": "byte" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "parent": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "dynamic": "false", + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "processor": { + "properties": { + "event": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "profile": { + "dynamic": "false", + "properties": { + "alloc_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "alloc_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cpu": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "duration": { + "type": "long" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "inuse_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "inuse_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "samples": { + "properties": { + "count": { + "type": "long" + } + } + }, + "stack": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + }, + "top": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "dynamic": "false", + "properties": { + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "framework": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "language": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "sourcemap": { + "dynamic": "false", + "properties": { + "bundle_filepath": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "db": { + "dynamic": "false", + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "rows_affected": { + "type": "long" + } + } + }, + "destination": { + "dynamic": "false", + "properties": { + "service": { + "dynamic": "false", + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "ignore_above": 1024, + "type": "keyword" + }, + "response_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "subtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cgroup": { + "properties": { + "memory": { + "properties": { + "mem": { + "properties": { + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "span": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "transaction": { + "dynamic": "false", + "properties": { + "breakdown": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "count": { + "type": "long" + }, + "histogram": { + "type": "histogram" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + }, + "us": { + "type": "long" + } + } + }, + "experience": { + "properties": { + "cls": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "fid": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "longtask": { + "properties": { + "count": { + "type": "long" + }, + "max": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "sum": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "tbt": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "marks": { + "dynamic": "true", + "properties": { + "*": { + "properties": { + "*": { + "dynamic": "true", + "type": "object" + } + } + } + } + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "root": { + "type": "boolean" + }, + "sampled": { + "type": "boolean" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "span_count": { + "properties": { + "dropped": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "dynamic": "false", + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "view spans": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "lifecycle": { + "name": "apm-rollover-30-days", + "rollover_alias": "apm-8.0.0-error" + }, + "mapping": { + "total_fields": { + "limit": "2000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "priority": "100", + "refresh_interval": "5s" + } + } + } +} + +{ + "type": "index", + "value": { + "index": "apm-8.0.0-transaction-2020.12.03-000005", + "mappings": { + "_meta": { + "beat": "apm", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "transaction.marks": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "transaction.marks.*" + } + }, + { + "transaction.marks.*.*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "transaction.marks.*.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "dynamic": "false", + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "path": "agent.name", + "type": "alias" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "child": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "dynamic": "false", + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "dynamic": "false", + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "culprit": { + "ignore_above": 1024, + "type": "keyword" + }, + "exception": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handled": { + "type": "boolean" + }, + "message": { + "norms": false, + "type": "text" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "grouping_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "param_message": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "experimental": { + "dynamic": "true", + "type": "object" + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "dynamic": "false", + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "dynamic": "false", + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "finished": { + "type": "boolean" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "dynamic": "false", + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "dynamic": "true", + "properties": { + "city": { + "type": "keyword" + }, + "company": { + "type": "keyword" + }, + "country_code": { + "type": "keyword" + }, + "customer_email": { + "type": "keyword" + }, + "customer_name": { + "type": "keyword" + }, + "customer_tier": { + "type": "keyword" + }, + "foo": { + "type": "keyword" + }, + "git_rev": { + "type": "keyword" + }, + "in_eu": { + "type": "boolean" + }, + "ip": { + "type": "keyword" + }, + "lang": { + "type": "keyword" + }, + "lorem": { + "type": "keyword" + }, + "multi-line": { + "type": "keyword" + }, + "request_id": { + "type": "keyword" + }, + "served_from_cache": { + "type": "keyword" + }, + "this-is-a-very-long-tag-name-without-any-spaces": { + "type": "keyword" + }, + "u": { + "type": "keyword" + }, + "worker": { + "type": "keyword" + } + } + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "metricset": { + "properties": { + "period": { + "type": "long" + } + } + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "dynamic": "false", + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "listening": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_major": { + "type": "byte" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "parent": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "dynamic": "false", + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "processor": { + "properties": { + "event": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "profile": { + "dynamic": "false", + "properties": { + "alloc_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "alloc_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cpu": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "duration": { + "type": "long" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "inuse_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "inuse_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "samples": { + "properties": { + "count": { + "type": "long" + } + } + }, + "stack": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + }, + "top": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "dynamic": "false", + "properties": { + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "framework": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "language": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "sourcemap": { + "dynamic": "false", + "properties": { + "bundle_filepath": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "db": { + "dynamic": "false", + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "rows_affected": { + "type": "long" + } + } + }, + "destination": { + "dynamic": "false", + "properties": { + "service": { + "dynamic": "false", + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "ignore_above": 1024, + "type": "keyword" + }, + "response_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "subtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cgroup": { + "properties": { + "memory": { + "properties": { + "mem": { + "properties": { + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "span": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "transaction": { + "dynamic": "false", + "properties": { + "breakdown": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "count": { + "type": "long" + }, + "histogram": { + "type": "histogram" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + }, + "us": { + "type": "long" + } + } + }, + "experience": { + "properties": { + "cls": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "fid": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "longtask": { + "properties": { + "count": { + "type": "long" + }, + "max": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "sum": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "tbt": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "marks": { + "dynamic": "true", + "properties": { + "*": { + "properties": { + "*": { + "dynamic": "true", + "type": "object" + } + } + }, + "agent": { + "properties": { + "domComplete": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domInteractive": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "firstContentfulPaint": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "largestContentfulPaint": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "timeToFirstByte": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "navigationTiming": { + "properties": { + "connectEnd": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "connectStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domComplete": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domContentLoadedEventEnd": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domContentLoadedEventStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domInteractive": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domLoading": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domainLookupEnd": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "domainLookupStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "fetchStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "loadEventEnd": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "loadEventStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "requestStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "responseEnd": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "responseStart": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + } + } + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "root": { + "type": "boolean" + }, + "sampled": { + "type": "boolean" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "span_count": { + "properties": { + "dropped": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "dynamic": "false", + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "view spans": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "lifecycle": { + "name": "apm-rollover-30-days", + "rollover_alias": "apm-8.0.0-transaction" + }, + "mapping": { + "total_fields": { + "limit": "2000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "priority": "100", + "refresh_interval": "5s" + } + } + } +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts index 6fc8cb4c1d4e1..6abb701f98a1c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts @@ -32,26 +32,52 @@ export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) describe('when there is data', () => { before(async () => { await esArchiver.load('8.0.0'); - await esArchiver.load('rum_8.0.0'); + await esArchiver.load('rum_test_data'); }); after(async () => { await esArchiver.unload('8.0.0'); - await esArchiver.unload('rum_8.0.0'); + await esArchiver.unload('rum_test_data'); }); it('returns js errors', async () => { const response = await supertest.get( - '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + '/api/apm/rum-client/js-errors?start=2021-01-18T12%3A20%3A17.202Z&end=2021-01-18T12%3A25%3A17.203Z&uiFilters=%7B%22environment%22%3A%22ENVIRONMENT_ALL%22%2C%22serviceName%22%3A%5B%22elastic-co-frontend%22%5D%7D&pageSize=5&pageIndex=0' ); expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` Object { - "items": Array [], - "totalErrorGroups": 0, - "totalErrorPages": 0, - "totalErrors": 0, + "items": Array [ + Object { + "count": 5, + "errorGroupId": "de32dc81e2ee5165cbff20046c080a27", + "errorMessage": "SyntaxError: Document.querySelector: '' is not a valid selector", + }, + Object { + "count": 2, + "errorGroupId": "34d83587e17711a7c257ffb080ddb1c6", + "errorMessage": "Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': The provided selector is empty.", + }, + Object { + "count": 43, + "errorGroupId": "3dd5604267b928139d958706f09f7e09", + "errorMessage": "Script error.", + }, + Object { + "count": 1, + "errorGroupId": "cd3a2b01017ff7bcce70479644f28318", + "errorMessage": "Unhandled promise rejection: TypeError: can't convert undefined to object", + }, + Object { + "count": 3, + "errorGroupId": "23539422cf714db071aba087dd041859", + "errorMessage": "Unable to get property 'left' of undefined or null reference", + }, + ], + "totalErrorGroups": 6, + "totalErrorPages": 120, + "totalErrors": 2846, } `); }); From c702b5cfe84ae7159669e39ce67b00e04bd528fa Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 20 Jan 2021 17:30:39 +0100 Subject: [PATCH 04/28] Skip snapshot_restore API integration tests in cloud (#88841) This PR disables the snapshot restore API integration tests in cloud. --- .../apis/management/snapshot_restore/snapshot_restore.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts index 575da0db2a759..1778e4f1210e5 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts @@ -24,6 +24,8 @@ export default function ({ getService }: FtrProviderContext) { } = registerEsHelpers(getService); describe('Snapshot Lifecycle Management', function () { + this.tags(['skipCloud']); // file system repositories are not supported in cloud + before(async () => { try { await createRepository(REPO_NAME); From ef408068055cd553e2409d5ab0fe686cca695584 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 20 Jan 2021 09:32:42 -0700 Subject: [PATCH 05/28] [Data/Search Sessions] Management UI (#81707) * logging and error handling in session client routes * [Data] Background Search Session Management UI * functional tests * fix ci * new functional tests * fix fn tests * cleanups * cleanup * restore test * configurable refresh and fetch timeout * more tests * feedback items * take expiresSoon field out of the interface * move helper to common * remove bg sessions w/find and delete * add storybook * fix tests * storybook actions * refactor expiration status calculation * isViewable as calculated field * refreshInterval 10s default * list newest first * "Type" => "App" * remove inline view action * in-progress status tooltip shows expire date * move date_string to public * fix tests * Adds management to tsconfig refs * removes preemptive script fix * view action was removed * rename the feature to Search Sessions * Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx Co-authored-by: Liza Katz * Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx Co-authored-by: Liza Katz * add TODO * use RedirectAppLinks * code review and react improvements * config * fix test * Fix merge * Fix management test * @Dosant code review * code review * Deleteed story * some more code review stuffs * fix ts * Code review and cleanup * Added functional tests for restoring, reloading and canceling a dashboard Renamed search session test service * Don't show expiration time for canceled, expired or errored sessions * fix jest * Moved UISession to public * @tsullivan code review * Fixed import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christiane Heiligers Co-authored-by: Liza Katz Co-authored-by: Liza K --- .../public/search/session/sessions_client.ts | 4 +- .../common/search/session/types.ts | 5 +- x-pack/plugins/data_enhanced/config.ts | 6 + x-pack/plugins/data_enhanced/kibana.json | 11 +- x-pack/plugins/data_enhanced/public/plugin.ts | 16 +- .../search/sessions_mgmt/__mocks__/index.tsx | 18 + .../sessions_mgmt/application/index.tsx | 78 + .../sessions_mgmt/application/render.tsx | 39 + .../components/actions/cancel_button.tsx | 89 + .../components/actions/extend_button.tsx | 89 + .../components/actions/get_action.tsx | 53 + .../components/actions/index.tsx | 8 + .../components/actions/popover_actions.tsx | 135 + .../components/actions/reload_button.tsx | 32 + .../sessions_mgmt/components/actions/types.ts | 13 + .../search/sessions_mgmt/components/index.tsx | 37 + .../sessions_mgmt/components/main.test.tsx | 93 + .../search/sessions_mgmt/components/main.tsx | 79 + .../sessions_mgmt/components/status.test.tsx | 132 + .../sessions_mgmt/components/status.tsx | 203 ++ .../components/table/app_filter.tsx | 27 + .../sessions_mgmt/components/table/index.ts | 7 + .../components/table/status_filter.tsx | 31 + .../components/table/table.test.tsx | 192 ++ .../sessions_mgmt/components/table/table.tsx | 122 + .../sessions_mgmt/icons/extend_session.svg | 3 + .../public/search/sessions_mgmt/index.ts | 64 + .../search/sessions_mgmt/lib/api.test.ts | 214 ++ .../public/search/sessions_mgmt/lib/api.ts | 182 ++ .../search/sessions_mgmt/lib/date_string.ts | 22 + .../search/sessions_mgmt/lib/documentation.ts | 22 + .../sessions_mgmt/lib/get_columns.test.tsx | 208 ++ .../search/sessions_mgmt/lib/get_columns.tsx | 233 ++ .../lib/get_expiration_status.ts | 47 + .../public/search/sessions_mgmt/types.ts | 22 + .../search_session_indicator.tsx | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 2 +- .../server/routes/session.test.ts | 6 +- .../data_enhanced/server/routes/session.ts | 9 +- .../search/session/session_service.test.ts | 1 + x-pack/plugins/data_enhanced/tsconfig.json | 2 + .../data/search_sessions/data.json.gz | Bin 0 -> 1956 bytes .../data/search_sessions/mappings.json | 2596 +++++++++++++++++ x-pack/test/functional/page_objects/index.ts | 2 + .../search_sessions_management_page.ts | 60 + .../config.ts | 1 + .../services/index.ts | 2 +- .../services/send_to_background.ts | 65 +- .../async_search/send_to_background.ts | 22 +- .../send_to_background_relative_time.ts | 10 +- .../async_search/sessions_in_space.ts | 10 +- .../tests/apps/discover/sessions_in_space.ts | 10 +- .../apps/management/search_sessions/index.ts | 24 + .../search_sessions/sessions_management.ts | 148 + 54 files changed, 5448 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts create mode 100644 x-pack/test/functional/es_archives/data/search_sessions/data.json.gz create mode 100644 x-pack/test/functional/es_archives/data/search_sessions/mappings.json create mode 100644 x-pack/test/functional/page_objects/search_sessions_management_page.ts create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index f4ad2df530d12..5b0ba51c2f344 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -56,8 +56,8 @@ export class SessionsClient { }); } - public find(options: SavedObjectsFindOptions): Promise { - return this.http!.post(`/internal/session`, { + public find(options: Omit): Promise { + return this.http!.post(`/internal/session/_find`, { body: JSON.stringify(options), }); } diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts index ada7988c31f30..9eefdf43aa245 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchSessionStatus } from './'; + export interface SearchSessionSavedObjectAttributes { /** * User-facing session name to be displayed in session management @@ -24,7 +26,7 @@ export interface SearchSessionSavedObjectAttributes { /** * status */ - status: string; + status: SearchSessionStatus; /** * urlGeneratorId */ @@ -44,7 +46,6 @@ export interface SearchSessionSavedObjectAttributes { */ idMapping: Record; } - export interface SearchSessionRequestInfo { /** * ID of the async search request diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index 4c90b1fb4c81d..981c398019832 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -15,6 +15,12 @@ export const configSchema = schema.object({ inMemTimeout: schema.duration({ defaultValue: '1m' }), maxUpdateRetries: schema.number({ defaultValue: 3 }), defaultExpiration: schema.duration({ defaultValue: '7d' }), + management: schema.object({ + maxSessions: schema.number({ defaultValue: 10000 }), + refreshInterval: schema.duration({ defaultValue: '10s' }), + refreshTimeout: schema.duration({ defaultValue: '1m' }), + expiresSoonWarning: schema.duration({ defaultValue: '1d' }), + }), }), }), }); diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index 3951468f6e569..037f52fcb4b05 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -2,15 +2,8 @@ "id": "dataEnhanced", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", "data_enhanced" - ], - "requiredPlugins": [ - "bfetch", - "data", - "features", - "taskManager" - ], + "configPath": ["xpack", "data_enhanced"], + "requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"], "optionalPlugins": ["kibanaUtils", "usageCollection"], "server": true, "ui": true, diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index fed2b4e71ab50..add7a966fee34 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -8,10 +8,13 @@ import React from 'react'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { SharePluginStart } from '../../../../src/plugins/share/public'; import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; import { EnhancedSearchInterceptor } from './search/search_interceptor'; +import { registerSearchSessionsMgmt } from './search/sessions_mgmt'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; import { createConnectedSearchSessionIndicator } from './search'; import { ConfigSchema } from '../config'; @@ -19,9 +22,11 @@ import { ConfigSchema } from '../config'; export interface DataEnhancedSetupDependencies { bfetch: BfetchPublicSetup; data: DataPublicPluginSetup; + management: ManagementSetup; } export interface DataEnhancedStartDependencies { data: DataPublicPluginStart; + share: SharePluginStart; } export type DataEnhancedSetup = ReturnType; @@ -30,12 +35,13 @@ export type DataEnhancedStart = ReturnType; export class DataEnhancedPlugin implements Plugin { private enhancedSearchInterceptor!: EnhancedSearchInterceptor; + private config!: ConfigSchema; constructor(private initializerContext: PluginInitializerContext) {} public setup( core: CoreSetup, - { bfetch, data }: DataEnhancedSetupDependencies + { bfetch, data, management }: DataEnhancedSetupDependencies ) { data.autocomplete.addQuerySuggestionProvider( KUERY_LANGUAGE_NAME, @@ -57,12 +63,18 @@ export class DataEnhancedPlugin searchInterceptor: this.enhancedSearchInterceptor, }, }); + + this.config = this.initializerContext.config.get(); + if (this.config.search.sessions.enabled) { + const { management: sessionsMgmtConfig } = this.config.search.sessions; + registerSearchSessionsMgmt(core, sessionsMgmtConfig, { management }); + } } public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { setAutocompleteService(plugins.data.autocomplete); - if (this.initializerContext.config.get().search.sessions.enabled) { + if (this.config.search.sessions.enabled) { core.chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( React.createElement( diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx new file mode 100644 index 0000000000000..e9fc8e6ac6bf9 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UrlGeneratorsStart } from '../../../../../../../src/plugins/share/public/url_generators'; + +export function LocaleWrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +export const mockUrls = ({ + getUrlGenerator: (id: string) => ({ createUrl: () => `hello-cool-${id}-url` }), +} as unknown) as UrlGeneratorsStart; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx new file mode 100644 index 0000000000000..27f1482a4d20d --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/public'; +import type { ManagementAppMountParams } from 'src/plugins/management/public'; +import type { + AppDependencies, + IManagementSectionsPluginsSetup, + IManagementSectionsPluginsStart, + SessionsMgmtConfigSchema, +} from '../'; +import { APP } from '../'; +import { SearchSessionsMgmtAPI } from '../lib/api'; +import { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { renderApp } from './render'; + +export class SearchSessionsMgmtApp { + constructor( + private coreSetup: CoreSetup, + private config: SessionsMgmtConfigSchema, + private params: ManagementAppMountParams, + private pluginsSetup: IManagementSectionsPluginsSetup + ) {} + + public async mountManagementSection() { + const { coreSetup, params, pluginsSetup } = this; + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + + const { + chrome: { docTitle }, + http, + docLinks, + i18n, + notifications, + uiSettings, + application, + } = coreStart; + const { data, share } = pluginsStart; + + const pluginName = APP.getI18nName(); + docTitle.change(pluginName); + params.setBreadcrumbs([{ text: pluginName }]); + + const { sessionsClient } = data.search; + const api = new SearchSessionsMgmtAPI(sessionsClient, this.config, { + notifications, + urls: share.urlGenerators, + application, + }); + + const documentation = new AsyncSearchIntroDocumentation(docLinks); + + const dependencies: AppDependencies = { + plugins: pluginsSetup, + config: this.config, + documentation, + core: coreStart, + api, + http, + i18n, + uiSettings, + share, + }; + + const { element } = params; + const unmountAppCb = renderApp(element, dependencies); + + return () => { + docTitle.reset(); + unmountAppCb(); + }; + } +} + +export { renderApp }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx new file mode 100644 index 0000000000000..f5ee35fcff9a9 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppDependencies } from '../'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { SearchSessionsMgmtMain } from '../components/main'; + +export const renderApp = ( + elem: HTMLElement | null, + { i18n, uiSettings, ...homeDeps }: AppDependencies +) => { + if (!elem) { + return () => undefined; + } + + const { Context: I18nContext } = i18n; + // uiSettings is required by the listing table to format dates in the timezone from Settings + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + }); + + render( + + + + + , + elem + ); + + return () => { + unmountComponentAtNode(elem); + }; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx new file mode 100644 index 0000000000000..8f4c8845de235 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { TableText } from '../'; +import { OnActionComplete } from './types'; + +interface CancelButtonProps { + id: string; + name: string; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +const CancelConfirm = ({ + onConfirmDismiss, + ...props +}: CancelButtonProps & { onConfirmDismiss: () => void }) => { + const { id, name, api, onActionComplete } = props; + const [isLoading, setIsLoading] = useState(false); + + const title = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.title', { + defaultMessage: 'Cancel search session', + }); + const confirm = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.cancelButton', { + defaultMessage: 'Cancel', + }); + const cancel = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.dontCancelButton', { + defaultMessage: 'Dismiss', + }); + const message = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.message', { + defaultMessage: `Canceling the search session \'{name}\' will expire any cached results, so that quick restore will no longer be available. You will still be able to re-run it, using the reload action.`, + values: { + name, + }, + }); + + return ( + + { + setIsLoading(true); + await api.sendCancel(id); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={cancel} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + + + ); +}; + +export const CancelButton = (props: CancelButtonProps) => { + const [showConfirm, setShowConfirm] = useState(false); + + const onClick = () => { + setShowConfirm(true); + }; + + const onConfirmDismiss = () => { + setShowConfirm(false); + }; + + return ( + <> + + + + {showConfirm ? : null} + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx new file mode 100644 index 0000000000000..4c8a7b0217688 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { TableText } from '../'; +import { OnActionComplete } from './types'; + +interface ExtendButtonProps { + id: string; + name: string; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +const ExtendConfirm = ({ + onConfirmDismiss, + ...props +}: ExtendButtonProps & { onConfirmDismiss: () => void }) => { + const { id, name, api, onActionComplete } = props; + const [isLoading, setIsLoading] = useState(false); + + const title = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.title', { + defaultMessage: 'Extend search session expiration', + }); + const confirm = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendButton', { + defaultMessage: 'Extend', + }); + const extend = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.dontExtendButton', { + defaultMessage: 'Cancel', + }); + const message = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendMessage', { + defaultMessage: "When would you like the search session '{name}' to expire?", + values: { + name, + }, + }); + + return ( + + { + setIsLoading(true); + await api.sendExtend(id, '1'); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={extend} + defaultFocusedButton="confirm" + buttonColor="primary" + > + {message} + + + ); +}; + +export const ExtendButton = (props: ExtendButtonProps) => { + const [showConfirm, setShowConfirm] = useState(false); + + const onClick = () => { + setShowConfirm(true); + }; + + const onConfirmDismiss = () => { + setShowConfirm(false); + }; + + return ( + <> + + + + {showConfirm ? : null} + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx new file mode 100644 index 0000000000000..5bf0fbda5b5cc --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { IClickActionDescriptor } from '../'; +import extendSessionIcon from '../../icons/extend_session.svg'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { UISession } from '../../types'; +import { CancelButton } from './cancel_button'; +import { ExtendButton } from './extend_button'; +import { ReloadButton } from './reload_button'; +import { ACTION, OnActionComplete } from './types'; + +export const getAction = ( + api: SearchSessionsMgmtAPI, + actionType: string, + { id, name, reloadUrl }: UISession, + onActionComplete: OnActionComplete +): IClickActionDescriptor | null => { + switch (actionType) { + case ACTION.CANCEL: + return { + iconType: 'crossInACircleFilled', + textColor: 'default', + label: , + }; + + case ACTION.RELOAD: + return { + iconType: 'refresh', + textColor: 'default', + label: , + }; + + case ACTION.EXTEND: + return { + iconType: extendSessionIcon, + textColor: 'default', + label: , + }; + + default: + // eslint-disable-next-line no-console + console.error(`Unknown action: ${actionType}`); + } + + // Unknown action: do not show + + return null; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx new file mode 100644 index 0000000000000..82b4d84aa7ea2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PopoverActionsMenu } from './popover_actions'; +export * from './types'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx new file mode 100644 index 0000000000000..b9b915c0b17cb --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.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; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiTextProps, + EuiToolTip, +} from '@elastic/eui'; +import { + EuiContextMenuPanelItemDescriptorEntry, + EuiContextMenuPanelItemSeparator, +} from '@elastic/eui/src/components/context_menu/context_menu'; +import { i18n } from '@kbn/i18n'; +import React, { ReactElement, useState } from 'react'; +import { TableText } from '../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { UISession } from '../../types'; +import { getAction } from './get_action'; +import { ACTION, OnActionComplete } from './types'; + +// interfaces +interface PopoverActionProps { + textColor?: EuiTextProps['color']; + iconType: string; + children: string | ReactElement; +} + +interface PopoverActionItemsProps { + session: UISession; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +// helper +const PopoverAction = ({ textColor, iconType, children, ...props }: PopoverActionProps) => ( + + + + + + {children} + + +); + +export const PopoverActionsMenu = ({ api, onActionComplete, session }: PopoverActionItemsProps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onPopoverClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const renderPopoverButton = () => ( + + + + ); + + const actions = session.actions || []; + // Generic set of actions - up to the API to return what is available + const items = actions.reduce((itemSet, actionType) => { + const actionDef = getAction(api, actionType, session, onActionComplete); + if (actionDef) { + const { label, textColor, iconType } = actionDef; + + // add a line above the delete action (when there are multiple) + // NOTE: Delete action MUST be the final action[] item + if (actions.length > 1 && actionType === ACTION.CANCEL) { + itemSet.push({ isSeparator: true, key: 'separadorable' }); + } + + return [ + ...itemSet, + { + key: `action-${actionType}`, + name: ( + + {label} + + ), + }, + ]; + } + return itemSet; + }, [] as Array); + + const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, items }]; + + return actions.length ? ( + + + + ) : null; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx new file mode 100644 index 0000000000000..9a98ab2044770 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { TableText } from '../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; + +interface ReloadButtonProps { + api: SearchSessionsMgmtAPI; + reloadUrl: string; +} + +export const ReloadButton = (props: ReloadButtonProps) => { + function onClick() { + props.api.reloadSearchSession(props.reloadUrl); + } + + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts new file mode 100644 index 0000000000000..4b81fd7fda9a0 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type OnActionComplete = () => void; + +export enum ACTION { + EXTEND = 'extend', + CANCEL = 'cancel', + RELOAD = 'reload', +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx new file mode 100644 index 0000000000000..ffb0992469a8a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLinkProps, EuiText, EuiTextProps } from '@elastic/eui'; +import React from 'react'; +import extendSessionIcon from '../icons/extend_session.svg'; + +export { OnActionComplete, PopoverActionsMenu } from './actions'; + +export const TableText = ({ children, ...props }: EuiTextProps) => { + return ( + + {children} + + ); +}; + +export interface IClickActionDescriptor { + label: string | React.ReactElement; + iconType: 'trash' | 'cancel' | typeof extendSessionIcon; + textColor: EuiTextProps['color']; +} + +export interface IHrefActionDescriptor { + label: string; + props: EuiLinkProps; +} + +export interface StatusDef { + textColor?: EuiTextProps['color']; + icon?: React.ReactElement; + label: React.ReactElement; + toolTipContent: string; +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx new file mode 100644 index 0000000000000..e01d1a28c5e54 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockedKeys } from '@kbn/utility-types/jest'; +import { mount, ReactWrapper } from 'enzyme'; +import { CoreSetup, CoreStart, DocLinksStart } from 'kibana/public'; +import moment from 'moment'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SessionsMgmtConfigSchema } from '..'; +import { SearchSessionsMgmtAPI } from '../lib/api'; +import { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { LocaleWrapper, mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtMain } from './main'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: MockedKeys; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; +let api: SearchSessionsMgmtAPI; + +describe('Background Search Session Management Main', () => { + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + }); + + describe('renders', () => { + const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'boo/', + DOC_LINK_VERSION: '#foo', + links: {} as any, + }; + + let main: ReactWrapper; + + beforeEach(async () => { + mockCoreSetup.uiSettings.get.mockImplementation((key: string) => { + return key === 'dateFormat:tz' ? 'UTC' : null; + }); + + await act(async () => { + main = mount( + + + + ); + }); + }); + + test('page title', () => { + expect(main.find('h1').text()).toBe('Search Sessions'); + }); + + test('documentation link', () => { + const docLink = main.find('a[href]').first(); + expect(docLink.text()).toBe('Documentation'); + expect(docLink.prop('href')).toBe( + 'boo/guide/en/elasticsearch/reference/#foo/async-search-intro.html' + ); + }); + + test('table is present', () => { + expect(main.find(`[data-test-subj="search-sessions-mgmt-table"]`).exists()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx new file mode 100644 index 0000000000000..80c6a580dd183 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { CoreStart, HttpStart } from 'kibana/public'; +import React from 'react'; +import type { SessionsMgmtConfigSchema } from '../'; +import type { SearchSessionsMgmtAPI } from '../lib/api'; +import type { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { TableText } from './'; +import { SearchSessionsMgmtTable } from './table'; + +interface Props { + documentation: AsyncSearchIntroDocumentation; + core: CoreStart; + api: SearchSessionsMgmtAPI; + http: HttpStart; + timezone: string; + config: SessionsMgmtConfigSchema; +} + +export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) { + return ( + + + + + +

+ +

+ + + + + + + + + + +

+ +

+
+ + + + + + + ); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx new file mode 100644 index 0000000000000..706001ac42146 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTextProps, EuiToolTipProps } from '@elastic/eui'; +import { mount } from 'enzyme'; +import React from 'react'; +import { SearchSessionStatus } from '../../../../common/search'; +import { UISession } from '../types'; +import { LocaleWrapper } from '../__mocks__'; +import { getStatusText, StatusIndicator } from './status'; + +let tz: string; +let session: UISession; + +const mockNowTime = new Date(); +mockNowTime.setTime(1607026176061); + +describe('Background Search Session management status labels', () => { + beforeEach(() => { + tz = 'Browser'; + session = { + name: 'amazing test', + id: 'wtywp9u2802hahgp-gsla', + restoreUrl: '/app/great-app-url/#45', + reloadUrl: '/app/great-app-url/#45', + appId: 'security', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }; + }); + + describe('getStatusText', () => { + test('in progress', () => { + expect(getStatusText(SearchSessionStatus.IN_PROGRESS)).toBe('In progress'); + }); + test('expired', () => { + expect(getStatusText(SearchSessionStatus.EXPIRED)).toBe('Expired'); + }); + test('cancelled', () => { + expect(getStatusText(SearchSessionStatus.CANCELLED)).toBe('Cancelled'); + }); + test('complete', () => { + expect(getStatusText(SearchSessionStatus.COMPLETE)).toBe('Complete'); + }); + test('error', () => { + expect(getStatusText('error')).toBe('Error'); + }); + }); + + describe('StatusIndicator', () => { + test('render in progress', () => { + const statusIndicator = mount( + + + + ); + + const label = statusIndicator.find( + `.euiText[data-test-subj="sessionManagementStatusLabel"][data-test-status="in_progress"]` + ); + expect(label.text()).toMatchInlineSnapshot(`"In progress"`); + }); + + test('complete', () => { + session.status = SearchSessionStatus.COMPLETE; + + const statusIndicator = mount( + + + + ); + + const label = statusIndicator + .find(`[data-test-subj="sessionManagementStatusLabel"][data-test-status="complete"]`) + .first(); + expect((label.props() as EuiTextProps).color).toBe('secondary'); + expect(label.text()).toBe('Complete'); + }); + + test('complete - expires soon', () => { + session.status = SearchSessionStatus.COMPLETE; + + const statusIndicator = mount( + + + + ); + + const tooltip = statusIndicator.find('EuiToolTip'); + expect((tooltip.first().props() as EuiToolTipProps).content).toMatchInlineSnapshot( + `"Expires on 6 Dec, 2020, 19:19:32"` + ); + }); + + test('expired', () => { + session.status = SearchSessionStatus.EXPIRED; + + const statusIndicator = mount( + + + + ); + + const label = statusIndicator + .find(`[data-test-subj="sessionManagementStatusLabel"][data-test-status="expired"]`) + .first(); + expect(label.text()).toBe('Expired'); + }); + + test('error handling', () => { + session.status = SearchSessionStatus.COMPLETE; + (session as any).created = null; + (session as any).expires = null; + + const statusIndicator = mount( + + + + ); + + // no unhandled errors + const tooltip = statusIndicator.find('EuiToolTip'); + expect((tooltip.first().props() as EuiToolTipProps).content).toMatchInlineSnapshot( + `"Expires on unknown"` + ); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx new file mode 100644 index 0000000000000..8e0946c140287 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { ReactElement } from 'react'; +import { SearchSessionStatus } from '../../../../common/search'; +import { dateString } from '../lib/date_string'; +import { UISession } from '../types'; +import { StatusDef as StatusAttributes, TableText } from './'; + +// Shared helper function +export const getStatusText = (statusType: string): string => { + switch (statusType) { + case SearchSessionStatus.IN_PROGRESS: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.inProgress', { + defaultMessage: 'In progress', + }); + case SearchSessionStatus.EXPIRED: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.expired', { + defaultMessage: 'Expired', + }); + case SearchSessionStatus.CANCELLED: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.cancelled', { + defaultMessage: 'Cancelled', + }); + case SearchSessionStatus.COMPLETE: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.complete', { + defaultMessage: 'Complete', + }); + case SearchSessionStatus.ERROR: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.error', { + defaultMessage: 'Error', + }); + default: + // eslint-disable-next-line no-console + console.error(`Unknown status ${statusType}`); + return statusType; + } +}; + +interface StatusIndicatorProps { + now?: string; + session: UISession; + timezone: string; +} + +// Get the fields needed to show each status type +// can throw errors around date conversions +const getStatusAttributes = ({ + now, + session, + timezone, +}: StatusIndicatorProps): StatusAttributes | null => { + let expireDate: string; + if (session.expires) { + expireDate = dateString(session.expires!, timezone); + } else { + expireDate = i18n.translate('xpack.data.mgmt.searchSessions.status.expireDateUnknown', { + defaultMessage: 'unknown', + }); + } + + switch (session.status) { + case SearchSessionStatus.IN_PROGRESS: + try { + return { + textColor: 'default', + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate( + 'xpack.data.mgmt.searchSessions.status.message.createdOn', + { + defaultMessage: 'Expires on {expireDate}', + values: { expireDate }, + } + ), + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error(`Could not instantiate a createdDate object from: ${session.created}`); + } + + case SearchSessionStatus.EXPIRED: + try { + const toolTipContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.message.expiredOn', + { + defaultMessage: 'Expired on {expireDate}', + values: { expireDate }, + } + ); + + return { + icon: , + label: {getStatusText(session.status)}, + toolTipContent, + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error(`Could not instantiate an expiration Date object from: ${session.expires}`); + } + + case SearchSessionStatus.CANCELLED: + return { + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate('xpack.data.mgmt.searchSessions.status.message.cancelled', { + defaultMessage: 'Cancelled by user', + }), + }; + + case SearchSessionStatus.ERROR: + return { + textColor: 'danger', + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate('xpack.data.mgmt.searchSessions.status.message.error', { + defaultMessage: 'Error: {error}', + values: { error: (session as any).error || 'unknown' }, + }), + }; + + case SearchSessionStatus.COMPLETE: + try { + const toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresOn', { + defaultMessage: 'Expires on {expireDate}', + values: { expireDate }, + }); + + return { + textColor: 'secondary', + icon: , + label: {getStatusText(session.status)}, + toolTipContent, + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error( + `Could not instantiate an expiration Date object for completed session from: ${session.expires}` + ); + } + + // Error was thrown + return null; + + default: + throw new Error(`Unknown status: ${session.status}`); + } +}; + +export const StatusIndicator = (props: StatusIndicatorProps) => { + try { + const statusDef = getStatusAttributes(props); + const { session } = props; + + if (statusDef) { + const { toolTipContent } = statusDef; + let icon: ReactElement | undefined = statusDef.icon; + let label: ReactElement = statusDef.label; + + if (icon && toolTipContent) { + icon = {icon}; + } + if (toolTipContent) { + label = ( + + + {statusDef.label} + + + ); + } + + return ( + + {icon} + + + {label} + + + + ); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + + // Exception has been caught + return {props.session.status}; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx new file mode 100644 index 0000000000000..236fc492031c0 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueOptionType, SearchFilterConfig } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { capitalize } from 'lodash'; +import { UISession } from '../../types'; + +export const getAppFilter: (tableData: UISession[]) => SearchFilterConfig = (tableData) => ({ + type: 'field_value_selection', + name: i18n.translate('xpack.data.mgmt.searchSessions.search.filterApp', { + defaultMessage: 'App', + }), + field: 'appId', + multiSelect: 'or', + options: tableData.reduce((options: FieldValueOptionType[], { appId }) => { + const existingOption = options.find((o) => o.value === appId); + if (!existingOption) { + return [...options, { value: appId, view: capitalize(appId) }]; + } + + return options; + }, []), +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts new file mode 100644 index 0000000000000..83ca1c223dfc4 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SearchSessionsMgmtTable } from './table'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx new file mode 100644 index 0000000000000..04421ad66e588 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueOptionType, SearchFilterConfig } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { TableText } from '../'; +import { UISession } from '../../types'; +import { getStatusText } from '../status'; + +export const getStatusFilter: (tableData: UISession[]) => SearchFilterConfig = (tableData) => ({ + type: 'field_value_selection', + name: i18n.translate('xpack.data.mgmt.searchSessions.search.filterStatus', { + defaultMessage: 'Status', + }), + field: 'status', + multiSelect: 'or', + options: tableData.reduce((options: FieldValueOptionType[], session) => { + const { status: statusType } = session; + const existingOption = options.find((o) => o.value === statusType); + if (!existingOption) { + const view = {getStatusText(session.status)}; + return [...options, { value: statusType, view }]; + } + + return options; + }, []), +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx new file mode 100644 index 0000000000000..357f17649394b --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockedKeys } from '@kbn/utility-types/jest'; +import { act, waitFor } from '@testing-library/react'; +import { mount, ReactWrapper } from 'enzyme'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import React from 'react'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SearchSessionStatus } from '../../../../../common/search'; +import { SessionsMgmtConfigSchema } from '../../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { LocaleWrapper, mockUrls } from '../../__mocks__'; +import { SearchSessionsMgmtTable } from './table'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: CoreStart; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; +let api: SearchSessionsMgmtAPI; + +describe('Background Search Session Management Table', () => { + beforeEach(async () => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + }); + + describe('renders', () => { + let table: ReactWrapper; + + const getInitialResponse = () => { + return { + saved_objects: [ + { + id: 'wtywp9u2802hahgp-flps', + attributes: { + name: 'very background search', + id: 'wtywp9u2802hahgp-flps', + url: '/app/great-app-url/#48', + appId: 'canvas', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }, + }, + ], + }; + }; + + test('table header cells', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return getInitialResponse(); + }); + + await act(async () => { + table = mount( + + + + ); + }); + + expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(` + Array [ + "AppClick to sort in ascending order", + "NameClick to sort in ascending order", + "StatusClick to sort in ascending order", + "CreatedClick to unsort", + "ExpirationClick to sort in ascending order", + ] + `); + }); + + test('table body cells', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return getInitialResponse(); + }); + + await act(async () => { + table = mount( + + + + ); + }); + table.update(); + + expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` + Array [ + "App", + "Namevery background search", + "StatusIn progress", + "Created2 Dec, 2020, 00:19:32", + "Expiration7 Dec, 2020, 00:19:32", + "", + "", + ] + `); + }); + }); + + describe('fetching sessions data', () => { + test('re-fetches data', async () => { + jest.useFakeTimers(); + sessionsClient.find = jest.fn(); + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(10, 'seconds'), + }; + + await act(async () => { + mount( + + + + ); + jest.advanceTimersByTime(20000); + }); + + // 1 for initial load + 2 refresh calls + expect(sessionsClient.find).toBeCalledTimes(3); + + jest.useRealTimers(); + }); + + test('refresh button uses the session client', async () => { + sessionsClient.find = jest.fn(); + + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(1, 'day'), + refreshTimeout: moment.duration(2, 'days'), + }; + + await act(async () => { + const table = mount( + + + + ); + + const buttonSelector = `[data-test-subj="sessionManagementRefreshBtn"] button`; + + await waitFor(() => { + table.find(buttonSelector).first().simulate('click'); + table.update(); + }); + }); + + // initial call + click + expect(sessionsClient.find).toBeCalledTimes(2); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx new file mode 100644 index 0000000000000..f7aecdbd58a23 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import moment from 'moment'; +import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import useInterval from 'react-use/lib/useInterval'; +import { TableText } from '../'; +import { SessionsMgmtConfigSchema } from '../..'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { getColumns } from '../../lib/get_columns'; +import { UISession } from '../../types'; +import { OnActionComplete } from '../actions'; +import { getAppFilter } from './app_filter'; +import { getStatusFilter } from './status_filter'; + +const TABLE_ID = 'searchSessionsMgmtTable'; + +interface Props { + core: CoreStart; + api: SearchSessionsMgmtAPI; + timezone: string; + config: SessionsMgmtConfigSchema; +} + +export function SearchSessionsMgmtTable({ core, api, timezone, config, ...props }: Props) { + const [tableData, setTableData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const showLatestResultsHandler = useRef(); + const refreshInterval = useMemo(() => moment.duration(config.refreshInterval).asMilliseconds(), [ + config.refreshInterval, + ]); + + // Debounce rendering the state of the Refresh button + useDebounce( + () => { + setDebouncedIsLoading(isLoading); + }, + 250, + [isLoading] + ); + + // refresh behavior + const doRefresh = useCallback(async () => { + setIsLoading(true); + const renderResults = (results: UISession[]) => { + setTableData(results); + }; + showLatestResultsHandler.current = renderResults; + let results: UISession[] = []; + try { + results = await api.fetchTableData(); + } catch (e) {} // eslint-disable-line no-empty + + if (showLatestResultsHandler.current === renderResults) { + renderResults(results); + setIsLoading(false); + } + }, [api]); + + // initial data load + useEffect(() => { + doRefresh(); + }, [doRefresh]); + + useInterval(doRefresh, refreshInterval); + + const onActionComplete: OnActionComplete = () => { + doRefresh(); + }; + + // table config: search / filters + const search: EuiSearchBarProps = { + box: { incremental: true }, + filters: [getStatusFilter(tableData), getAppFilter(tableData)], + toolsRight: ( + + + + + + ), + }; + + return ( + + {...props} + id={TABLE_ID} + data-test-subj={TABLE_ID} + rowProps={() => ({ + 'data-test-subj': 'searchSessionsRow', + })} + columns={getColumns(core, api, config, timezone, onActionComplete)} + items={tableData} + pagination={pagination} + search={search} + sorting={{ sort: { field: 'created', direction: 'desc' } }} + onTableChange={({ page: { index } }) => { + setPagination({ pageIndex: index }); + }} + tableLayout="auto" + /> + ); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg new file mode 100644 index 0000000000000..7cb9f7e6a24c2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts new file mode 100644 index 0000000000000..76a5d440cd898 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import type { CoreStart, HttpStart, I18nStart, IUiSettingsClient } from 'kibana/public'; +import { CoreSetup } from 'kibana/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { ManagementSetup } from 'src/plugins/management/public'; +import type { SharePluginStart } from 'src/plugins/share/public'; +import type { ConfigSchema } from '../../../config'; +import type { DataEnhancedStartDependencies } from '../../plugin'; +import type { SearchSessionsMgmtAPI } from './lib/api'; +import type { AsyncSearchIntroDocumentation } from './lib/documentation'; + +export interface IManagementSectionsPluginsSetup { + management: ManagementSetup; +} + +export interface IManagementSectionsPluginsStart { + data: DataPublicPluginStart; + share: SharePluginStart; +} + +export interface AppDependencies { + plugins: IManagementSectionsPluginsSetup; + share: SharePluginStart; + uiSettings: IUiSettingsClient; + documentation: AsyncSearchIntroDocumentation; + core: CoreStart; // for RedirectAppLinks + api: SearchSessionsMgmtAPI; + http: HttpStart; + i18n: I18nStart; + config: SessionsMgmtConfigSchema; +} + +export const APP = { + id: 'search_sessions', + getI18nName: (): string => + i18n.translate('xpack.data.mgmt.searchSessions.appTitle', { + defaultMessage: 'Search Sessions', + }), +}; + +export type SessionsMgmtConfigSchema = ConfigSchema['search']['sessions']['management']; + +export function registerSearchSessionsMgmt( + coreSetup: CoreSetup, + config: SessionsMgmtConfigSchema, + services: IManagementSectionsPluginsSetup +) { + services.management.sections.section.kibana.registerApp({ + id: APP.id, + title: APP.getI18nName(), + order: 2, + mount: async (params) => { + const { SearchSessionsMgmtApp: MgmtApp } = await import('./application'); + const mgmtApp = new MgmtApp(coreSetup, config, params, services); + return mgmtApp.mountManagementSection(); + }, + }); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts new file mode 100644 index 0000000000000..5b337dfd03eb1 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { MockedKeys } from '@kbn/utility-types/jest'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import { coreMock } from 'src/core/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { SavedObjectsFindResponse } from 'src/core/server'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import type { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtAPI } from './api'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: MockedKeys; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; + +describe('Search Sessions Management API', () => { + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration('1d'), + maxSessions: 2000, + refreshInterval: moment.duration('1s'), + refreshTimeout: moment.duration('10m'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + }); + + describe('listing', () => { + test('fetchDataTable calls the listing endpoint', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: 'complete' }, + }, + ], + } as SavedObjectsFindResponse; + }); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + expect(await api.fetchTableData()).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Array [ + "reload", + "extend", + "cancel", + ], + "appId": "pizza", + "created": undefined, + "expires": undefined, + "id": "hello-pizza-123", + "name": "Veggie", + "reloadUrl": "hello-cool-undefined-url", + "restoreUrl": "hello-cool-undefined-url", + "status": "complete", + }, + ] + `); + }); + + test('handle error from sessionsClient response', async () => { + sessionsClient.find = jest.fn().mockRejectedValue(new Error('implementation is so bad')); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.fetchTableData(); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('implementation is so bad'), + { title: 'Failed to refresh the page!' } + ); + }); + + test('handle timeout error', async () => { + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(1, 'hours'), + refreshTimeout: moment.duration(1, 'seconds'), + }; + + sessionsClient.find = jest.fn().mockImplementation(async () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.fetchTableData(); + + expect(mockCoreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'Fetching the Search Session info timed out after 1 seconds' + ); + }); + }); + + describe('cancel', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: 'baked' }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send cancel calls the cancel endpoint with a session ID', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendCancel('abc-123-cool-session-ID'); + + expect(mockCoreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'The search session was canceled and expired.', + }); + }); + + test('error if deleting shows a toast message', async () => { + sessionsClient.delete = jest.fn().mockRejectedValue(new Error('implementation is so bad')); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendCancel('abc-123-cool-session-ID'); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('implementation is so bad'), + { title: 'Failed to cancel the search session!' } + ); + }); + }); + + describe('reload', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: SearchSessionStatus.COMPLETE }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send cancel calls the cancel endpoint with a session ID', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.reloadSearchSession('www.myurl.com'); + + expect(mockCoreStart.application.navigateToUrl).toHaveBeenCalledWith('www.myurl.com'); + }); + }); + + describe('extend', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: SearchSessionStatus.COMPLETE }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send extend throws an error for now', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendExtend('my-id', '5d'); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts new file mode 100644 index 0000000000000..a2bd6b1a549be --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import type { ApplicationStart, NotificationsStart, SavedObject } from 'kibana/public'; +import moment from 'moment'; +import { from, race, timer } from 'rxjs'; +import { mapTo, tap } from 'rxjs/operators'; +import type { SharePluginStart } from 'src/plugins/share/public'; +import { SessionsMgmtConfigSchema } from '../'; +import type { ISessionsClient } from '../../../../../../../src/plugins/data/public'; +import type { SearchSessionSavedObjectAttributes } from '../../../../common'; +import { SearchSessionStatus } from '../../../../common/search'; +import { ACTION } from '../components/actions'; +import { UISession } from '../types'; + +type UrlGeneratorsStart = SharePluginStart['urlGenerators']; + +function getActions(status: SearchSessionStatus) { + const actions: ACTION[] = []; + actions.push(ACTION.RELOAD); + if (status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE) { + actions.push(ACTION.EXTEND); + actions.push(ACTION.CANCEL); + } + return actions; +} + +async function getUrlFromState( + urls: UrlGeneratorsStart, + urlGeneratorId: string, + state: Record +) { + let url = '/'; + try { + url = await urls.getUrlGenerator(urlGeneratorId).createUrl(state); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Could not create URL from restoreState'); + // eslint-disable-next-line no-console + console.error(err); + } + return url; +} + +// Helper: factory for a function to map server objects to UI objects +const mapToUISession = ( + urls: UrlGeneratorsStart, + { expiresSoonWarning }: SessionsMgmtConfigSchema +) => async (savedObject: SavedObject): Promise => { + const { + name, + appId, + created, + expires, + status, + urlGeneratorId, + initialState, + restoreState, + } = savedObject.attributes; + + const actions = getActions(status); + + // TODO: initialState should be saved without the searchSessionID + if (initialState) delete initialState.searchSessionId; + // derive the URL and add it in + const reloadUrl = await getUrlFromState(urls, urlGeneratorId, initialState); + const restoreUrl = await getUrlFromState(urls, urlGeneratorId, restoreState); + + return { + id: savedObject.id, + name, + appId, + created, + expires, + status, + actions, + restoreUrl, + reloadUrl, + }; +}; + +interface SearcgSessuibManagementDeps { + urls: UrlGeneratorsStart; + notifications: NotificationsStart; + application: ApplicationStart; +} + +export class SearchSessionsMgmtAPI { + constructor( + private sessionsClient: ISessionsClient, + private config: SessionsMgmtConfigSchema, + private deps: SearcgSessuibManagementDeps + ) {} + + public async fetchTableData(): Promise { + interface FetchResult { + saved_objects: object[]; + } + + const refreshTimeout = moment.duration(this.config.refreshTimeout); + + const fetch$ = from( + this.sessionsClient.find({ + page: 1, + perPage: this.config.maxSessions, + sortField: 'created', + sortOrder: 'asc', + }) + ); + const timeout$ = timer(refreshTimeout.asMilliseconds()).pipe( + tap(() => { + this.deps.notifications.toasts.addDanger( + i18n.translate('xpack.data.mgmt.searchSessions.api.fetchTimeout', { + defaultMessage: 'Fetching the Search Session info timed out after {timeout} seconds', + values: { timeout: refreshTimeout.asSeconds() }, + }) + ); + }), + mapTo(null) + ); + + // fetch the search sessions before timeout triggers + try { + const result = await race(fetch$, timeout$).toPromise(); + if (result && result.saved_objects) { + const savedObjects = result.saved_objects as Array< + SavedObject + >; + return await Promise.all(savedObjects.map(mapToUISession(this.deps.urls, this.config))); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.fetchError', { + defaultMessage: 'Failed to refresh the page!', + }), + }); + } + + return []; + } + + public reloadSearchSession(reloadUrl: string) { + this.deps.application.navigateToUrl(reloadUrl); + } + + // Cancel and expire + public async sendCancel(id: string): Promise { + try { + await this.sessionsClient.delete(id); + + this.deps.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.data.mgmt.searchSessions.api.canceled', { + defaultMessage: 'The search session was canceled and expired.', + }), + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.cancelError', { + defaultMessage: 'Failed to cancel the search session!', + }), + }); + } + } + + // Extend + public async sendExtend(id: string, ttl: string): Promise { + this.deps.notifications.toasts.addError(new Error('Not implemented'), { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { + defaultMessage: 'Failed to extend the session expiration!', + }), + }); + } +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts new file mode 100644 index 0000000000000..7640d8b80766e --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { DATE_STRING_FORMAT } from '../types'; + +export const dateString = (inputString: string, tz: string): string => { + if (inputString == null) { + throw new Error('Invalid date string!'); + } + let returnString: string; + if (tz === 'Browser') { + returnString = moment.utc(inputString).tz(moment.tz.guess()).format(DATE_STRING_FORMAT); + } else { + returnString = moment(inputString).tz(tz).format(DATE_STRING_FORMAT); + } + + return returnString; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts new file mode 100644 index 0000000000000..eac3245dfe2bc --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DocLinksStart } from 'kibana/public'; + +export class AsyncSearchIntroDocumentation { + private docsBasePath: string = ''; + + constructor(docs: DocLinksStart) { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docs; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + // TODO: There should be Kibana documentation link about Search Sessions in Kibana + this.docsBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + } + + public getElasticsearchDocLink() { + return `${this.docsBasePath}/async-search-intro.html`; + } +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx new file mode 100644 index 0000000000000..ce441efea7385 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTableFieldDataColumnType } from '@elastic/eui'; +import { MockedKeys } from '@kbn/utility-types/jest'; +import { mount } from 'enzyme'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import { ReactElement } from 'react'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { OnActionComplete } from '../components'; +import { UISession } from '../types'; +import { mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtAPI } from './api'; +import { getColumns } from './get_columns'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: CoreStart; +let mockConfig: SessionsMgmtConfigSchema; +let api: SearchSessionsMgmtAPI; +let sessionsClient: SessionsClient; +let handleAction: OnActionComplete; +let mockSession: UISession; + +let tz = 'UTC'; + +describe('Search Sessions Management table column factory', () => { + beforeEach(async () => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + tz = 'UTC'; + + handleAction = () => { + throw new Error('not testing handle action'); + }; + + mockSession = { + name: 'Cool mock session', + id: 'wtywp9u2802hahgp-thao', + reloadUrl: '/app/great-app-url', + restoreUrl: '/app/great-app-url/#42', + appId: 'discovery', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }; + }); + + test('returns columns', () => { + const columns = getColumns(mockCoreStart, api, mockConfig, tz, handleAction); + expect(columns).toMatchInlineSnapshot(` + Array [ + Object { + "field": "appId", + "name": "App", + "render": [Function], + "sortable": true, + }, + Object { + "field": "name", + "name": "Name", + "render": [Function], + "sortable": true, + "width": "20%", + }, + Object { + "field": "status", + "name": "Status", + "render": [Function], + "sortable": true, + }, + Object { + "field": "created", + "name": "Created", + "render": [Function], + "sortable": true, + }, + Object { + "field": "expires", + "name": "Expiration", + "render": [Function], + "sortable": true, + }, + Object { + "field": "status", + "name": "", + "render": [Function], + "sortable": false, + }, + Object { + "field": "actions", + "name": "", + "render": [Function], + "sortable": false, + }, + ] + `); + }); + + describe('name', () => { + test('rendering', () => { + const [, nameColumn] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + const name = mount(nameColumn.render!(mockSession.name, mockSession) as ReactElement); + + expect(name.text()).toBe('Cool mock session'); + }); + }); + + // Status column + describe('status', () => { + test('render in_progress', () => { + const [, , status] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement); + expect( + statusLine.find('.euiText[data-test-subj="sessionManagementStatusTooltip"]').text() + ).toMatchInlineSnapshot(`"In progress"`); + }); + + test('error handling', () => { + const [, , status] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + mockSession.status = 'INVALID' as SearchSessionStatus; + const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement); + + // no unhandled error + + expect(statusLine.text()).toMatchInlineSnapshot(`"INVALID"`); + }); + }); + + // Start Date column + describe('startedDate', () => { + test('render using Browser timezone', () => { + tz = 'Browser'; + + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + expect(date.text()).toBe('1 Dec, 2020, 19:19:32'); + }); + + test('render using AK timezone', () => { + tz = 'US/Alaska'; + + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + expect(date.text()).toBe('1 Dec, 2020, 15:19:32'); + }); + + test('error handling', () => { + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + mockSession.created = 'INVALID'; + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + // no unhandled error + expect(date.text()).toBe('Invalid date'); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx new file mode 100644 index 0000000000000..090336c37a98f --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiLink, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; +import { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { TableText } from '../components'; +import { OnActionComplete, PopoverActionsMenu } from '../components'; +import { StatusIndicator } from '../components/status'; +import { dateString } from '../lib/date_string'; +import { SearchSessionsMgmtAPI } from './api'; +import { getExpirationStatus } from './get_expiration_status'; +import { UISession } from '../types'; + +// Helper function: translate an app string to EuiIcon-friendly string +const appToIcon = (app: string) => { + if (app === 'dashboards') { + return 'dashboard'; + } + return app; +}; + +function isSessionRestorable(status: SearchSessionStatus) { + return status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE; +} + +export const getColumns = ( + core: CoreStart, + api: SearchSessionsMgmtAPI, + config: SessionsMgmtConfigSchema, + timezone: string, + onActionComplete: OnActionComplete +): Array> => { + // Use a literal array of table column definitions to detail a UISession object + return [ + // App + { + field: 'appId', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerType', { + defaultMessage: 'App', + }), + sortable: true, + render: (appId: UISession['appId'], { id }) => { + const app = `${appToIcon(appId)}`; + return ( + + + + ); + }, + }, + + // Name, links to app and displays the search session data + { + field: 'name', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerName', { + defaultMessage: 'Name', + }), + sortable: true, + width: '20%', + render: (name: UISession['name'], { restoreUrl, reloadUrl, status }) => { + const isRestorable = isSessionRestorable(status); + const notRestorableWarning = isRestorable ? null : ( + <> + {' '} + + } + /> + + ); + return ( + + + + {name} + {notRestorableWarning} + + + + ); + }, + }, + + // Session status + { + field: 'status', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerStatus', { + defaultMessage: 'Status', + }), + sortable: true, + render: (statusType: UISession['status'], session) => ( + + ), + }, + + // Started date + { + field: 'created', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerStarted', { + defaultMessage: 'Created', + }), + sortable: true, + render: (created: UISession['created'], { id }) => { + try { + const startedOn = dateString(created, timezone); + return ( + + {startedOn} + + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + return {created}; + } + }, + }, + + // Expiration date + { + field: 'expires', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerExpiration', { + defaultMessage: 'Expiration', + }), + sortable: true, + render: (expires: UISession['expires'], { id, status }) => { + if ( + expires && + status !== SearchSessionStatus.EXPIRED && + status !== SearchSessionStatus.CANCELLED && + status !== SearchSessionStatus.ERROR + ) { + try { + const expiresOn = dateString(expires, timezone); + + // return + return ( + + {expiresOn} + + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + return {expires}; + } + } + return ( + + -- + + ); + }, + }, + + // Highlight Badge, if completed session expires soon + { + field: 'status', + name: '', + sortable: false, + render: (status, { expires }) => { + const expirationStatus = getExpirationStatus(config, expires); + if (expirationStatus) { + const { toolTipContent, statusContent } = expirationStatus; + + return ( + + + {statusContent} + + + ); + } + + return ; + }, + }, + + // Action(s) in-line in the row, additional action(s) in the popover, no column header + { + field: 'actions', + name: '', + sortable: false, + render: (actions: UISession['actions'], session) => { + if (actions && actions.length) { + return ( + + + + + + ); + } + }, + }, + ]; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts new file mode 100644 index 0000000000000..3c167d6dbe41a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { SessionsMgmtConfigSchema } from '../'; + +export const getExpirationStatus = (config: SessionsMgmtConfigSchema, expires: string | null) => { + const tNow = moment.utc().valueOf(); + const tFuture = moment.utc(expires).valueOf(); + + // NOTE this could end up negative. If server time is off from the browser's clock + // and the session was early expired when the browser refreshed the listing + const durationToExpire = moment.duration(tFuture - tNow); + const expiresInDays = Math.floor(durationToExpire.asDays()); + const sufficientDays = Math.ceil(moment.duration(config.expiresSoonWarning).asDays()); + + let toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresSoonInDays', { + defaultMessage: 'Expires in {numDays} days', + values: { numDays: expiresInDays }, + }); + let statusContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.expiresSoonInDaysTooltip', + { defaultMessage: '{numDays} days', values: { numDays: expiresInDays } } + ); + + if (expiresInDays === 0) { + // switch to show expires in hours + const expiresInHours = Math.floor(durationToExpire.asHours()); + + toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresSoonInHours', { + defaultMessage: 'This session expires in {numHours} hours', + values: { numHours: expiresInHours }, + }); + statusContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.expiresSoonInHoursTooltip', + { defaultMessage: '{numHours} hours', values: { numHours: expiresInHours } } + ); + } + + if (durationToExpire.valueOf() > 0 && expiresInDays <= sufficientDays) { + return { toolTipContent, statusContent }; + } +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts new file mode 100644 index 0000000000000..78b91f7ca8ac2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchSessionStatus } from '../../../common'; +import { ACTION } from './components/actions'; + +export const DATE_STRING_FORMAT = 'D MMM, YYYY, HH:mm:ss'; + +export interface UISession { + id: string; + name: string; + appId: string; + created: string; + expires: string | null; + status: SearchSessionStatus; + actions?: ACTION[]; + reloadUrl: string; + restoreUrl: string; +} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index ed022e18c34d7..361688581b4f1 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -66,7 +66,7 @@ const ContinueInBackgroundButton = ({ ); const ViewAllSearchSessionsButton = ({ - viewSearchSessionsLink = 'management', + viewSearchSessionsLink = 'management/kibana/search_sessions', buttonProps = {}, }: ActionButtonProps) => ( { let mockCoreSetup: MockedKeys>; let mockContext: jest.Mocked; + let mockLogger: Logger; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockLogger = coreMock.createPluginInitializerContext().logger.get(); mockContext = createSearchRequestHandlerContext(); - registerSessionRoutes(mockCoreSetup.http.createRouter()); + registerSessionRoutes(mockCoreSetup.http.createRouter(), mockLogger); }); it('save calls session.save with sessionId and attributes', async () => { diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts index b056513f1d2f5..9e61dd39c83b8 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -5,10 +5,10 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from 'src/core/server'; +import { IRouter, Logger } from 'src/core/server'; import { reportServerError } from '../../../../../src/plugins/kibana_utils/server'; -export function registerSessionRoutes(router: IRouter): void { +export function registerSessionRoutes(router: IRouter, logger: Logger): void { router.post( { path: '/internal/session', @@ -49,6 +49,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } @@ -73,6 +74,7 @@ export function registerSessionRoutes(router: IRouter): void { }); } catch (e) { const err = e.output?.payload || e; + logger.error(err); return reportServerError(res, err); } } @@ -106,6 +108,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } @@ -128,6 +131,7 @@ export function registerSessionRoutes(router: IRouter): void { return res.ok(); } catch (e) { const err = e.output?.payload || e; + logger.error(err); return reportServerError(res, err); } } @@ -156,6 +160,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index f37aaf71fded5..1107ed8155080 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -114,6 +114,7 @@ describe('SearchSessionService', () => { maxUpdateRetries: 3, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + management: {} as any, }, }, }); diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index ec5c656ac50b5..c4b09276880d9 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -12,6 +12,7 @@ "public/**/*", "server/**/*", "config.ts", + "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json" ], @@ -22,6 +23,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../features/tsconfig.json" }, diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..28260ee99e4dc0c428d5e2a75a0a14db990e2723 GIT binary patch literal 1956 zcmZ9Jc|6m79KeSh9TP&B^*E=Nk)y(zq+{8LFt-&g=T?br)XoBaqK zZlb5cW0S}AEXNoT5lf!+Jg?`U=dbr4-`D5+{(Rrx-{^93mpp!)0Z9@3BK(X(!Y^I` z&E|N!3|c71j)?ZMhHD8@t*9J_!wEVeYp7D*Bil)Yt)84uT*GW^y?+%tW0T~j_N zzst5vQ8k>o9AWg>JlB#6&98gttUIN4dY6VVvJGq(`ZJnql9pJL>7vx5_0)vfo3wJ0 zm3VFZV)qQe-qf_Ifzv&slMBw(%amrO9g0HJ)7DeU{dKH=H$&}Ee|VATdgPqf2g3sW zs0HTY>NpDmF@Xo!(IIcgOOmo#HGYpKq#u8Fr?oBcCa~!0ZFGWH=l$d{oHKOA$)?of#1DMyBX+L6o41yzk9C_(?owkc!ja#8) zSFKe`6{9S(9R&&Uz##~kG(F&(EU3kn@X31slr3MbD(^Bs1Y+cNAhKv9R&g@w(YKK& z2bZg=1d+@6OR%NBhiRoxkE;?n?DFb*)91vb9oZ+MaJYG>26g1S!bjMn!!Z-@ai)2) z4YOA!(61N4T?cV+FSwW6`IVYm$_=k9dKZs2nsPr*SH7DDh`r=8G&9G(I9O@RZ8X`= zNU&SSE)wE1zC*)sC+}4Iial`Us$Rxt7lMV$83j(koLub`S*?4`GFSqf-x=t>NSmv?hGnI{*?t4V;vxS0q#YCy8rrK1v z&w*%Wa9G!_FBz}#YCC13($j>jfLA(R731|6%TTAB0c8K0z${7$X`OGTTb9o%Jj4^Y6p*AwLY%5 zwi*A*uW&zL&(q9|uw`Q$)VS3OEa7~$(GT+Z+{q0LALi+-uLpJgfdR%eQ?KA7Z+!U9 zJe11VMOK5UL&9|%2DQld7Y~G;IH@=>dDFu9csT#13E%3lj9%mTr@`SZ0h93&l9tNo z7O*aaYS)rrh5sC}UO7p9Y4LR}k4`dp+O`tdNqb~+Brt?dkJdp+hS%3#d}r=Br=ycfv^N6<3a8F1s>9t#`9 z)~~l{=V9g8s}hSB-@MLWXTKcl z95}&vo*K3o(_ZBuL?ZnMdtyn_cX2#GVo+h;F@Dy4m5}W`kp-IVAGm}Lx@FdTc!&R> zMq;^5RT6LJdWod}(drFz|1Vo_N#SeW$05IZrNJU$wZmT>oseT*jeaT>zHjz#5~cfA zZr)mFE`7G(l|#M%t~e(JXpeq750&^ts?sJvb6Vy?wE)RhxWCAT zm`L&UM30ia4dp!BWh!F8wQiR@ZiFU-xC+8G76T*%cnU0r1A&j|dPY40B(=f`d#8z@ z0fo84vI9}ji6=GL8`wo5t66`l42Bx34x)=}dcwn;gLZRVp%SI(k{E<`9|wjafhgEA zIlK4!iJ{_?1&?^rZVoM$`(SBPP)%z>$`Vn@`zW(;i+HD_O#Nf^K!L%09YU2>5Gcjf zz#wp%ZewC-^Y+6~5P?seM(KfE=>K=I>hqLGQQ*=zXrrTehu_- z0-(@;)Sm7CSCju$lmD+S)BQ(XcJ4p*=%+YmtQ_TzCmKZYCW<@3SO{=o23+-7iWmxW z*(^rv48YsYVr+E{Eg`#_c*KX4kZDfbY;uIee}7S)*MGY8oC=WFuPBK>*RS|wNEKq+ zUu8!Ga~N>tXNh7c%zk1q-pl|V;^JVRUpIw$#EU^Y%Y8uJboN&#vd7gFt@+=FP|z literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json new file mode 100644 index 0000000000000..24bbcbea23385 --- /dev/null +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -0,0 +1,2596 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "49eb3350984bd2a162914d3776e70cfb", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "0cbbb16506734d341a96aaed65ec6413", + "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "meta": { + "properties": { + "versionApiKeyLastmodified": { + "type": "keyword" + } + } + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "notifyWhen": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "api_key_pending_invalidation": { + "properties": { + "apiKeyId": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "urlGeneratorId": { + "type": "keyword" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "alertId": { + "type": "keyword" + }, + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "index": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enterprise_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_source": { + "type": "keyword" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "package_assets": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages-assets": { + "properties": { + "asset_path": { + "type": "keyword" + }, + "data_base64": { + "type": "binary" + }, + "data_utf8": { + "index": false, + "type": "text" + }, + "install_source": { + "type": "keyword" + }, + "media_type": { + "type": "keyword" + }, + "package_name": { + "type": "keyword" + }, + "package_version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "ack_data": { + "type": "text" + }, + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "legacyIndexPatternRef": { + "index": false, + "type": "text" + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "dynamic": "false", + "type": "object" + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "config_yaml": { + "type": "text" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "compiled_input": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_urls": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "dynamic": "false", + "type": "object" + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-job": { + "properties": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "indexNames": { + "type": "text" + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaces-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "tag": { + "properties": { + "color": { + "type": "text" + }, + "description": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 4c523ec5706e1..20b8acb9d4509 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -38,6 +38,7 @@ import { SpaceSelectorPageProvider } from './space_selector_page'; import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; +import { SearchSessionsPageProvider } from './search_sessions_management_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -64,6 +65,7 @@ export const pageObjects = { apiKeys: ApiKeysPageProvider, licenseManagement: LicenseManagementPageProvider, indexManagement: IndexManagementPageProvider, + searchSessionsManagement: SearchSessionsPageProvider, indexLifecycleManagement: IndexLifecycleManagementPageProvider, tagManagement: TagManagementPageProvider, snapshotRestore: SnapshotRestorePageProvider, diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts new file mode 100644 index 0000000000000..99c3be82a214d --- /dev/null +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + return { + async goTo() { + await PageObjects.common.navigateToApp('management/kibana/search_sessions'); + }, + + async refresh() { + await testSubjects.click('sessionManagementRefreshBtn'); + }, + + async getList() { + const table = await find.byCssSelector('table'); + const allRows = await table.findAllByTestSubject('searchSessionsRow'); + + return Promise.all( + allRows.map(async (row) => { + const $ = await row.parseDomContent(); + const viewCell = await row.findByTestSubject('sessionManagementNameCol'); + const actionsCell = await row.findByTestSubject('sessionManagementActionsCol'); + return { + name: $.findTestSubject('sessionManagementNameCol').text(), + status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'), + mainUrl: $.findTestSubject('sessionManagementNameCol').text(), + created: $.findTestSubject('sessionManagementCreatedCol').text(), + expires: $.findTestSubject('sessionManagementExpiresCol').text(), + app: $.findTestSubject('sessionManagementAppIcon').attr('data-test-app-id'), + view: async () => { + await viewCell.click(); + }, + reload: async () => { + await actionsCell.click(); + await find.clickByCssSelector( + '[data-test-subj="sessionManagementPopoverAction-reload"]' + ); + }, + cancel: async () => { + await actionsCell.click(); + await find.clickByCssSelector( + '[data-test-subj="sessionManagementPopoverAction-cancel"]' + ); + await PageObjects.common.clickConfirmOnModal(); + }, + }; + }) + ); + }, + }; +} diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index c14678febd811..bad818bb69664 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -23,6 +23,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/index.ts b/x-pack/test/send_search_to_background_integration/services/index.ts index 91b0ad502d053..35eed5a218b42 100644 --- a/x-pack/test/send_search_to_background_integration/services/index.ts +++ b/x-pack/test/send_search_to_background_integration/services/index.ts @@ -9,5 +9,5 @@ import { SendToBackgroundProvider } from './send_to_background'; export const services = { ...functionalServices, - sendToBackground: SendToBackgroundProvider, + searchSessions: SendToBackgroundProvider, }; diff --git a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts index 319496239de34..8c3261c2074ae 100644 --- a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { SavedObjectsFindResponse } from 'src/core/server'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../ftr_provider_context'; -const SEND_TO_BACKGROUND_TEST_SUBJ = 'searchSessionIndicator'; -const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer'; +const SEARCH_SESSION_INDICATOR_TEST_SUBJ = 'searchSessionIndicator'; +const SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer'; type SessionStateType = | 'none' @@ -21,22 +22,24 @@ type SessionStateType = export function SendToBackgroundProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); + const supertest = getService('supertest'); return new (class SendToBackgroundService { public async find(): Promise { - return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ); + return testSubjects.find(SEARCH_SESSION_INDICATOR_TEST_SUBJ); } public async exists(): Promise { - return testSubjects.exists(SEND_TO_BACKGROUND_TEST_SUBJ); + return testSubjects.exists(SEARCH_SESSION_INDICATOR_TEST_SUBJ); } public async expectState(state: SessionStateType) { - return retry.waitFor(`sendToBackground indicator to get into state = ${state}`, async () => { + return retry.waitFor(`searchSessions indicator to get into state = ${state}`, async () => { const currentState = await ( - await testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ) + await testSubjects.find(SEARCH_SESSION_INDICATOR_TEST_SUBJ) ).getAttribute('data-state'); return currentState === state; }); @@ -65,23 +68,57 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { await this.ensurePopoverClosed(); } + public async openPopover() { + await this.ensurePopoverOpened(); + } + private async ensurePopoverOpened() { - const isAlreadyOpen = await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + const isAlreadyOpen = await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ); if (isAlreadyOpen) return; - return retry.waitFor(`sendToBackground popover opened`, async () => { - await testSubjects.click(SEND_TO_BACKGROUND_TEST_SUBJ); - return await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + return retry.waitFor(`searchSessions popover opened`, async () => { + await testSubjects.click(SEARCH_SESSION_INDICATOR_TEST_SUBJ); + return await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ); }); } private async ensurePopoverClosed() { const isAlreadyClosed = !(await testSubjects.exists( - SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ + SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ )); if (isAlreadyClosed) return; - return retry.waitFor(`sendToBackground popover closed`, async () => { + return retry.waitFor(`searchSessions popover closed`, async () => { await browser.pressKeys(browser.keys.ESCAPE); - return !(await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ)); + return !(await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ)); + }); + } + + /* + * This cleanup function should be used by tests that create new background sesions. + * Tests should not end with new background sessions remaining in storage since that interferes with functional tests that check the _find API. + * Alternatively, a test can navigate to `Managment > Search Sessions` and use the UI to delete any created tests. + */ + public async deleteAllSearchSessions() { + log.debug('Deleting created background sessions'); + // ignores 409 errs and keeps retrying + await retry.tryForTime(10000, async () => { + const { body } = await supertest + .post('/internal/session/_find') + .set('kbn-xsrf', 'anything') + .set('kbn-system-request', 'true') + .send({ page: 1, perPage: 10000, sortField: 'created', sortOrder: 'asc' }) + .expect(200); + + const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; + log.debug(`Found created background sessions: ${savedObjects.map(({ id }) => id)}`); + await Promise.all( + savedObjects.map(async (so) => { + log.debug(`Deleting background session: ${so.id}`); + await supertest + .delete(`/internal/session/${so.id}`) + .set(`kbn-xsrf`, `anything`) + .expect(200); + }) + ); }); } })(); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts index 2edaeb1918b25..03635efb6113d 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('send to background', () => { before(async function () { @@ -26,6 +26,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); }); + after(async function () { + await searchSessions.deleteAllSearchSessions(); + }); + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); const url = await browser.getCurrentUrl(); @@ -33,7 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; await browser.get(savedSessionURL); await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session const session1 = await dashboardPanelActions.getSearchSessionIdByTitle( @@ -41,9 +45,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); expect(session1).to.be(fakeSessionId); - await sendToBackground.refresh(); + await searchSessions.refresh(); await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('completed'); + await searchSessions.expectState('completed'); await testSubjects.missingOrFail('embeddableErrorLabel'); const session2 = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension' @@ -54,9 +58,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Saves and restores a session', async () => { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); await PageObjects.dashboard.waitForRenderComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension' ); @@ -69,7 +73,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); expect(data.length).to.be(5); @@ -77,7 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // switching dashboard to edit mode (or any other non-fetch required) state change // should leave session state untouched await PageObjects.dashboard.switchToEditMode(); - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); }); }); } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts index 9eb42b74668c8..ce6c8978c7d67 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const dashboardExpect = getService('dashboardExpect'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('send to background with relative time', () => { before(async () => { @@ -60,9 +60,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await checkSampleDashboardLoaded(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( '[Flights] Airline Carrier' ); @@ -80,7 +80,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await checkSampleDashboardLoaded(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 7d00761b2fa9f..f590e44138642 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('dashboard in space', () => { describe('Send to background in space', () => { @@ -73,9 +73,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( 'A Pie in another space' ); @@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts index 5c94a50e0a84d..6384afb179593 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('discover in space', () => { describe('Send to background in space', () => { @@ -74,9 +74,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitForDocTableLoadingComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); await inspector.open(); const savedSessionId = await ( @@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitForDocTableLoadingComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts new file mode 100644 index 0000000000000..6a11a15f31567 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile, getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('search sessions management', function () { + this.tags('ciGroup3'); + + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('dashboard/async_search'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await kibanaServer.uiSettings.replace({ 'search:timeout': 10000 }); + }); + + loadTestFile(require.resolve('./sessions_management')); + }); +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts new file mode 100644 index 0000000000000..f06e8eba0bf68 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects([ + 'common', + 'header', + 'dashboard', + 'visChart', + 'searchSessionsManagement', + ]); + const searchSessions = getService('searchSessions'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + + describe('Search search sessions Management UI', () => { + describe('New search sessions', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + }); + + after(async () => { + await searchSessions.deleteAllSearchSessions(); + }); + + it('Saves a session and verifies it in the Management app', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + await PageObjects.dashboard.waitForRenderComplete(); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); + + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + await retry.waitFor(`wait for first item to complete`, async function () { + const s = await PageObjects.searchSessionsManagement.getList(); + return s[0] && s[0].status === 'complete'; + }); + + // find there is only one item in the table which is the newly saved session + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + expect(searchSessionList.length).to.be(1); + expect(searchSessionList[0].expires).not.to.eql('--'); + expect(searchSessionList[0].name).to.eql('Not Delayed'); + + // navigate to dashboard + await searchSessionList[0].view(); + + // embeddable has loaded + await testSubjects.existOrFail('embeddablePanelHeading-SumofBytesbyExtension'); + await PageObjects.dashboard.waitForRenderComplete(); + + // search session was restored + await searchSessions.expectState('restored'); + }); + + it('Reloads as new session from management', async () => { + await PageObjects.searchSessionsManagement.goTo(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(1); + await searchSessionList[0].reload(); + + // embeddable has loaded + await PageObjects.dashboard.waitForRenderComplete(); + + // new search session was completed + await searchSessions.expectState('completed'); + }); + + it('Cancels a session from management', async () => { + await PageObjects.searchSessionsManagement.goTo(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(1); + await searchSessionList[0].cancel(); + + // TODO: update this once canceling doesn't delete the object! + await retry.waitFor(`wait for list to be empty`, async function () { + const s = await PageObjects.searchSessionsManagement.getList(); + + return s.length === 0; + }); + }); + }); + + describe('Archived search sessions', () => { + before(async () => { + await PageObjects.searchSessionsManagement.goTo(); + }); + + after(async () => { + await searchSessions.deleteAllSearchSessions(); + }); + + it('shows no items found', async () => { + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + expect(searchSessionList.length).to.be(0); + }); + + it('autorefreshes and shows items on the server', async () => { + await esArchiver.load('data/search_sessions'); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(10); + + expect(searchSessionList.map((ss) => ss.created)).to.eql([ + '25 Dec, 2020, 00:00:00', + '24 Dec, 2020, 00:00:00', + '23 Dec, 2020, 00:00:00', + '22 Dec, 2020, 00:00:00', + '21 Dec, 2020, 00:00:00', + '20 Dec, 2020, 00:00:00', + '19 Dec, 2020, 00:00:00', + '18 Dec, 2020, 00:00:00', + '17 Dec, 2020, 00:00:00', + '16 Dec, 2020, 00:00:00', + ]); + + expect(searchSessionList.map((ss) => ss.expires)).to.eql([ + '--', + '--', + '--', + '23 Dec, 2020, 00:00:00', + '22 Dec, 2020, 00:00:00', + '--', + '--', + '--', + '18 Dec, 2020, 00:00:00', + '17 Dec, 2020, 00:00:00', + ]); + + await esArchiver.unload('data/search_sessions'); + }); + }); + }); +} From 86789dabb567c478ffa5b418d52566a3363085b9 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 20 Jan 2021 17:49:21 +0100 Subject: [PATCH 06/28] [Lens] Add more in-editor Advanced documentation (#86821) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Wylie Conlon Co-authored-by: Michael Marcialis --- ...ibana-plugin-plugins-data-public.search.md | 11 + .../lib/time_buckets/calc_auto_interval.ts | 194 +++++++++++++++--- src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 41 ++-- .../dimension_panel/dimension_editor.tsx | 31 ++- .../dimension_panel/dimension_panel.test.tsx | 3 + .../dimension_panel/reference_editor.tsx | 11 +- .../indexpattern_datasource/help_popover.scss | 13 ++ .../indexpattern_datasource/help_popover.tsx | 70 +++++++ .../calculations/moving_average.tsx | 85 +++++++- .../operations/definitions/date_histogram.tsx | 92 ++++++++- .../operations/definitions/index.ts | 8 + .../definitions/ranges/range_editor.tsx | 82 ++++++-- .../xy_visualization/xy_config_panel.scss | 2 +- .../xy_visualization/xy_config_panel.tsx | 23 ++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 17 files changed, 578 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/help_popover.scss create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/help_popover.tsx diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 22dc92c275670..4b3c915b49c2d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -35,6 +35,17 @@ search: { siblingPipelineType: string; termsAggFilter: string[]; toAbsoluteDates: typeof toAbsoluteDates; + boundsDescendingRaw: ({ + bound: number; + interval: import("moment").Duration; + boundLabel: string; + intervalLabel: string; + } | { + bound: import("moment").Duration; + interval: import("moment").Duration; + boundLabel: string; + intervalLabel: string; + })[]; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts index 83fd22a618fec..3c1a89015252e 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts @@ -6,75 +6,205 @@ * Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import moment from 'moment'; -const boundsDescending = [ +export const boundsDescendingRaw = [ { bound: Infinity, - interval: Number(moment.duration(1, 'year')), + interval: moment.duration(1, 'year'), + boundLabel: i18n.translate('data.search.timeBuckets.infinityLabel', { + defaultMessage: 'More than a year', + }), + intervalLabel: i18n.translate('data.search.timeBuckets.yearLabel', { + defaultMessage: 'a year', + }), }, { - bound: Number(moment.duration(1, 'year')), - interval: Number(moment.duration(1, 'month')), + bound: moment.duration(1, 'year'), + interval: moment.duration(1, 'month'), + boundLabel: i18n.translate('data.search.timeBuckets.yearLabel', { + defaultMessage: 'a year', + }), + intervalLabel: i18n.translate('data.search.timeBuckets.monthLabel', { + defaultMessage: 'a month', + }), }, { - bound: Number(moment.duration(3, 'week')), - interval: Number(moment.duration(1, 'week')), + bound: moment.duration(3, 'week'), + interval: moment.duration(1, 'week'), + boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', { + defaultMessage: '{amount, plural, one {a day} other {# days}}', + values: { amount: 21 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.dayLabel', { + defaultMessage: '{amount, plural, one {a day} other {# days}}', + values: { amount: 7 }, + }), }, { - bound: Number(moment.duration(1, 'week')), - interval: Number(moment.duration(1, 'd')), + bound: moment.duration(1, 'week'), + interval: moment.duration(1, 'd'), + boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', { + defaultMessage: '{amount, plural, one {a day} other {# days}}', + values: { amount: 7 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.dayLabel', { + defaultMessage: '{amount, plural, one {a day} other {# days}}', + values: { amount: 1 }, + }), }, { - bound: Number(moment.duration(24, 'hour')), - interval: Number(moment.duration(12, 'hour')), + bound: moment.duration(24, 'hour'), + interval: moment.duration(12, 'hour'), + boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', { + defaultMessage: '{amount, plural, one {a day} other {# days}}', + values: { amount: 1 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', { + defaultMessage: '{amount, plural, one {an hour} other {# hours}}', + values: { amount: 12 }, + }), }, { - bound: Number(moment.duration(6, 'hour')), - interval: Number(moment.duration(3, 'hour')), + bound: moment.duration(6, 'hour'), + interval: moment.duration(3, 'hour'), + boundLabel: i18n.translate('data.search.timeBuckets.hourLabel', { + defaultMessage: '{amount, plural, one {an hour} other {# hours}}', + values: { amount: 6 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', { + defaultMessage: '{amount, plural, one {an hour} other {# hours}}', + values: { amount: 3 }, + }), }, { - bound: Number(moment.duration(2, 'hour')), - interval: Number(moment.duration(1, 'hour')), + bound: moment.duration(2, 'hour'), + interval: moment.duration(1, 'hour'), + boundLabel: i18n.translate('data.search.timeBuckets.hourLabel', { + defaultMessage: '{amount, plural, one {an hour} other {# hours}}', + values: { amount: 2 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', { + defaultMessage: '{amount, plural, one {an hour} other {# hours}}', + values: { amount: 1 }, + }), }, { - bound: Number(moment.duration(45, 'minute')), - interval: Number(moment.duration(30, 'minute')), + bound: moment.duration(45, 'minute'), + interval: moment.duration(30, 'minute'), + boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 45 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 30 }, + }), }, { - bound: Number(moment.duration(20, 'minute')), - interval: Number(moment.duration(10, 'minute')), + bound: moment.duration(20, 'minute'), + interval: moment.duration(10, 'minute'), + boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 20 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 10 }, + }), }, { - bound: Number(moment.duration(9, 'minute')), - interval: Number(moment.duration(5, 'minute')), + bound: moment.duration(9, 'minute'), + interval: moment.duration(5, 'minute'), + boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 9 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 5 }, + }), }, { - bound: Number(moment.duration(3, 'minute')), - interval: Number(moment.duration(1, 'minute')), + bound: moment.duration(3, 'minute'), + interval: moment.duration(1, 'minute'), + boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 3 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', { + defaultMessage: '{amount, plural, one {a minute} other {# minutes}}', + values: { amount: 1 }, + }), }, { - bound: Number(moment.duration(45, 'second')), - interval: Number(moment.duration(30, 'second')), + bound: moment.duration(45, 'second'), + interval: moment.duration(30, 'second'), + boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 45 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 30 }, + }), }, { - bound: Number(moment.duration(15, 'second')), - interval: Number(moment.duration(10, 'second')), + bound: moment.duration(15, 'second'), + interval: moment.duration(10, 'second'), + boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 15 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 10 }, + }), }, { - bound: Number(moment.duration(7.5, 'second')), - interval: Number(moment.duration(5, 'second')), + bound: moment.duration(7.5, 'second'), + interval: moment.duration(5, 'second'), + boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 7.5 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 5 }, + }), }, { - bound: Number(moment.duration(5, 'second')), - interval: Number(moment.duration(1, 'second')), + bound: moment.duration(5, 'second'), + interval: moment.duration(1, 'second'), + boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 5 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', { + defaultMessage: '{amount, plural, one {a second} other {# seconds}}', + values: { amount: 1 }, + }), }, { - bound: Number(moment.duration(500, 'ms')), - interval: Number(moment.duration(100, 'ms')), + bound: moment.duration(500, 'ms'), + interval: moment.duration(100, 'ms'), + boundLabel: i18n.translate('data.search.timeBuckets.millisecondLabel', { + defaultMessage: '{amount, plural, one {a millisecond} other {# milliseconds}}', + values: { amount: 500 }, + }), + intervalLabel: i18n.translate('data.search.timeBuckets.millisecondLabel', { + defaultMessage: '{amount, plural, one {a millisecond} other {# milliseconds}}', + values: { amount: 100 }, + }), }, ]; +const boundsDescending = boundsDescendingRaw.map(({ bound, interval }) => ({ + bound: Number(bound), + interval: Number(interval), +})); + function getPerBucketMs(count: number, duration: number) { const ms = duration / count; return isFinite(ms) ? ms : NaN; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 9f0a5b64bde5a..ff3e2ebc89a41 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -307,6 +307,7 @@ import { parseEsInterval, parseInterval, toAbsoluteDates, + boundsDescendingRaw, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -416,6 +417,7 @@ export const search = { siblingPipelineType, termsAggFilter, toAbsoluteDates, + boundsDescendingRaw, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index dd24b1152b22c..e521e468d14a4 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2205,6 +2205,17 @@ export const search: { siblingPipelineType: string; termsAggFilter: string[]; toAbsoluteDates: typeof toAbsoluteDates; + boundsDescendingRaw: ({ + bound: number; + interval: import("moment").Duration; + boundLabel: string; + intervalLabel: string; + } | { + bound: import("moment").Duration; + interval: import("moment").Duration; + boundLabel: string; + intervalLabel: string; + })[]; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2608,21 +2619,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts 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 1bdffc90797ac..dc7b291b7120f 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 @@ -299,6 +299,17 @@ export function DimensionEditor(props: DimensionEditorProps) { } ); + // Need to workout early on the error to decide whether to show this or an help text + const fieldErrorMessage = + (selectedOperationDefinition?.input !== 'fullReference' || + (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field')) && + getErrorMessage( + selectedColumn, + Boolean(incompleteOperation), + selectedOperationDefinition?.input, + currentFieldIsInvalid + ); + return (
@@ -342,6 +353,11 @@ export function DimensionEditor(props: DimensionEditorProps) { existingFields={state.existingFields} selectionStyle={selectedOperationDefinition.selectionStyle} dateRange={dateRange} + labelAppend={selectedOperationDefinition?.getHelpMessage?.({ + data: props.data, + uiSettings: props.uiSettings, + currentColumn: state.layers[layerId].columns[columnId], + })} {...services} /> ); @@ -360,12 +376,15 @@ export function DimensionEditor(props: DimensionEditorProps) { })} fullWidth isInvalid={Boolean(incompleteOperation || currentFieldIsInvalid)} - error={getErrorMessage( - selectedColumn, - Boolean(incompleteOperation), - selectedOperationDefinition?.input, - currentFieldIsInvalid - )} + error={fieldErrorMessage} + labelAppend={ + !fieldErrorMessage && + selectedOperationDefinition?.getHelpMessage?.({ + data: props.data, + uiSettings: props.uiSettings, + currentColumn: state.layers[layerId].columns[columnId], + }) + } > { id: 'bytes', title: 'Bytes', }), + deserialize: jest.fn().mockReturnValue({ + convert: () => 'formatted', + }), } as unknown) as DataPublicPluginStart['fieldFormats'], } as unknown) as DataPublicPluginStart, core: {} as CoreSetup, 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 d73530ec8a920..1a394584360ad 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 @@ -8,7 +8,13 @@ import './dimension_editor.scss'; import _ from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EuiFormRow, + EuiFormRowProps, + EuiSpacer, + EuiComboBox, + EuiComboBoxOptionOption, +} from '@elastic/eui'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; @@ -40,6 +46,7 @@ export interface ReferenceEditorProps { currentIndexPattern: IndexPattern; existingFields: IndexPatternPrivateState['existingFields']; dateRange: DateRange; + labelAppend?: EuiFormRowProps['labelAppend']; // Services uiSettings: IUiSettingsClient; @@ -59,6 +66,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { validation, selectionStyle, dateRange, + labelAppend, ...services } = props; @@ -251,6 +259,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { })} fullWidth isInvalid={showFieldInvalid} + labelAppend={labelAppend} > { + return ( + + + + + {children} + + + ); +}; + +export const HelpPopover = ({ + anchorPosition, + button, + children, + closePopover, + isOpen, + title, +}: { + anchorPosition?: EuiPopoverProps['anchorPosition']; + button: EuiPopoverProps['button']; + children: ReactNode; + closePopover: EuiPopoverProps['closePopover']; + isOpen: EuiPopoverProps['isOpen']; + title?: string; +}) => { + return ( + + {title && {title}} + + + {children} + + + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index d9805b337c000..d43dbccd92f83 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -5,10 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { useState } from 'react'; -import React from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { EuiFieldNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; import { @@ -21,6 +20,7 @@ import { import { updateColumnParam } from '../../layer_helpers'; import { isValidNumber, useDebounceWithOptions } from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; +import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; const ofName = buildLabelFunction((name?: string) => { @@ -111,6 +111,7 @@ export const movingAverageOperation: OperationDefinition< }) ); }, + getHelpMessage: () => , getDisabledStatus(indexPattern, layer) { return checkForDateHistogram( layer, @@ -168,3 +169,79 @@ function MovingAverageParamEditor({ ); } + +const MovingAveragePopup = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + return ( + setIsPopoverOpen(!isPopoverOpen)}> + {i18n.translate('xpack.lens.indexPattern.movingAverage.helpText', { + defaultMessage: 'How it works', + })} + + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.lens.indexPattern.movingAverage.titleHelp', { + defaultMessage: 'How moving average works', + })} + > +

+ +

+ +

+ +

+ +

+ +

+ +
    +
  • (1 + 2 + 3 + 4 + 5) / 5 = 3
  • +
  • (2 + 3 + 4 + 5 + 6) / 5 = 4
  • +
  • ...
  • +
  • (5 + 6 + 7 + 8 + 9) / 5 = 7
  • +
+ +

+ +

+

+ +

+
    +
  • (1 + 2) / 2 = 1.5
  • +
  • (1 + 2 + 3) / 3 = 2
  • +
  • (1 + 2 + 3 + 4) / 4 = 2.5
  • +
  • (1 + 2 + 3 + 4 + 5) / 5 = 3
  • +
+ +

+ +

+
+ ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index a41cc88c4f292..2e61f4fc3e24d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -4,31 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiBasicTable, + EuiCode, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, + EuiSelect, + EuiSpacer, EuiSwitch, EuiSwitchEvent, - EuiFieldNumber, - EuiSelect, - EuiFlexItem, - EuiFlexGroup, EuiTextColor, - EuiSpacer, } from '@elastic/eui'; import { updateColumnParam } from '../layer_helpers'; import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; import { AggFunctionsMapping, + DataPublicPluginStart, IndexPatternAggRestrictions, search, + UI_SETTINGS, } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { getInvalidFieldMessage, getSafeName } from './helpers'; +import { HelpPopover, HelpPopoverButton } from '../../help_popover'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; @@ -54,6 +59,7 @@ export const dateHistogramOperation: OperationDefinition< priority: 5, // Highest priority level used getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + getHelpMessage: (props) => , getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'date' && @@ -334,3 +340,77 @@ function restrictedInterval(aggregationRestrictions?: Partial { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const infiniteBound = i18n.translate('xpack.lens.indexPattern.dateHistogram.moreThanYear', { + defaultMessage: 'More than a year', + }); + const upToLabel = i18n.translate('xpack.lens.indexPattern.dateHistogram.upTo', { + defaultMessage: 'Up to', + }); + + return ( + setIsPopoverOpen(!isPopoverOpen)}> + {i18n.translate('xpack.lens.indexPattern.dateHistogram.autoHelpText', { + defaultMessage: 'How it works', + })} + + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.lens.indexPattern.dateHistogram.titleHelp', { + defaultMessage: 'How auto date histogram works', + })} + > +

+ {i18n.translate('xpack.lens.indexPattern.dateHistogram.autoBasicExplanation', { + defaultMessage: 'The auto date histogram splits a date field into buckets by interval.', + })} +

+ +

+ {UI_SETTINGS.HISTOGRAM_MAX_BARS}, + targetBarSetting: {UI_SETTINGS.HISTOGRAM_BAR_TARGET}, + }} + /> +

+ +

+ {i18n.translate('xpack.lens.indexPattern.dateHistogram.autoAdvancedExplanation', { + defaultMessage: 'The interval follows this logic:', + })} +

+ + ({ + bound: typeof bound === 'number' ? infiniteBound : `${upToLabel} ${boundLabel}`, + interval: intervalLabel, + }))} + columns={[ + { + field: 'bound', + name: i18n.translate('xpack.lens.indexPattern.dateHistogram.autoBoundHeader', { + defaultMessage: 'Target interval measured', + }), + }, + { + field: 'interval', + name: i18n.translate('xpack.lens.indexPattern.dateHistogram.autoIntervalHeader', { + defaultMessage: 'Interval used', + }), + }, + ]} + /> +
+ ); +}; 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 36c9cf75d2b6c..7dbc7d3b986a5 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 @@ -126,6 +126,12 @@ export interface ParamEditorProps { data: DataPublicPluginStart; } +export interface HelpProps { + currentColumn: C; + uiSettings: IUiSettingsClient; + data: DataPublicPluginStart; +} + export type TimeScalingMode = 'disabled' | 'mandatory' | 'optional'; interface BaseOperationDefinitionProps { @@ -201,6 +207,8 @@ interface BaseOperationDefinitionProps { * If set to optional, time scaling won't be enabled by default and can be removed. */ timeScalingMode?: TimeScalingMode; + + getHelpMessage?: (props: HelpProps) => React.ReactNode; } interface BaseBuildColumnArgs { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index df955be6b490a..ad5c146ff6624 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -6,21 +6,73 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, + EuiButtonIcon, + EuiCode, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, EuiRange, - EuiFlexItem, - EuiFlexGroup, - EuiButtonIcon, EuiToolTip, - EuiIconTip, } from '@elastic/eui'; -import { IFieldFormat } from 'src/plugins/data/public'; +import type { IFieldFormat } from 'src/plugins/data/public'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges'; import { AdvancedRangeEditor } from './advanced_editor'; import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants'; import { useDebounceWithOptions } from '../helpers'; +import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; + +const GranularityHelpPopover = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(!isPopoverOpen)}> + {i18n.translate('xpack.lens.indexPattern.ranges.granularityHelpText', { + defaultMessage: 'How it works', + })} + + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverTitle', { + defaultMessage: 'How granularity interval works', + })} + > +

+ {i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverBasicExplanation', { + defaultMessage: + 'Interval granularity divides the field into evenly spaced intervals based on the minimum and maximum values for the field.', + })} +

+ +

+ {UI_SETTINGS.HISTOGRAM_MAX_BARS}, + }} + /> +

+ +

+ {i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverAdvancedExplanation', { + defaultMessage: + 'Intervals are incremented by 10, 5 or 2: for example an interval can be 100 or 0.2 .', + })} +

+
+ ); +}; const BaseRangeEditor = ({ maxBars, @@ -49,12 +101,7 @@ const BaseRangeEditor = ({ const granularityLabel = i18n.translate('xpack.lens.indexPattern.ranges.granularity', { defaultMessage: 'Intervals granularity', }); - const granularityLabelDescription = i18n.translate( - 'xpack.lens.indexPattern.ranges.granularityDescription', - { - defaultMessage: 'Divides the field into evenly spaced intervals.', - } - ); + const decreaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.decreaseButtonLabel', { defaultMessage: 'Decrease granularity', }); @@ -65,21 +112,12 @@ const BaseRangeEditor = ({ return ( <> - {granularityLabel}{' '} - - - } + label={granularityLabel} data-test-subj="indexPattern-ranges-section-label" labelType="legend" fullWidth display="rowCompressed" + labelAppend={} > diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss index b9ff6a56d8e35..a2caeb93477fa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss @@ -1,3 +1,3 @@ .lnsXyToolbar__popover { - width: 320px; + width: 365px; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 351b1f0d71651..b8bca09bb353c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -21,6 +21,7 @@ import { EuiColorPickerProps, EuiToolTip, EuiIcon, + EuiIconTip, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { @@ -327,9 +328,25 @@ export function XyToolbar(props: VisualizationToolbarProps) { {isFittingEnabled ? ( + {i18n.translate('xpack.lens.xyChart.missingValuesLabel', { + defaultMessage: 'Missing values', + })}{' '} + + + } > Date: Wed, 20 Jan 2021 10:52:03 -0600 Subject: [PATCH 07/28] [ML] Redesign file-based Data Visualizer (#87598) --- .../ml/common/types/file_datavisualizer.ts | 18 +- .../ml/common/types/ml_url_generator.ts | 13 +- .../components/data_grid/column_chart.scss | 1 + .../components/data_grid/use_column_chart.tsx | 2 +- .../field_title_bar/field_title_bar.tsx | 12 +- .../analytics_list/use_table_settings.ts | 10 +- .../application/datavisualizer/_index.scss | 2 +- .../file_based/components/_index.scss | 1 - .../expanded_row/file_based_expanded_row.tsx} | 49 ++--- .../components/expanded_row/index.ts | 7 + .../components/field_data_row/index.ts | 7 + .../field_data_row/number_content_preview.tsx | 55 +++++ .../field_names_filter/field_names_filter.tsx | 46 ++++ .../components/field_names_filter}/index.ts | 2 +- .../field_types_filter/field_types_filter.tsx | 79 +++++++ .../components/field_types_filter/index.ts | 7 + .../components/fields_stats/_index.scss | 2 - .../fields_stats/field_stats_card.js | 184 ---------------- .../components/fields_stats/fields_stats.js | 113 ---------- .../fields_stats_grid/create_fields.ts | 126 +++++++++++ .../fields_stats_grid/fields_stats_grid.tsx | 124 +++++++++++ .../fields_stats_grid/filter_fields.ts | 36 ++++ .../get_field_names.ts} | 28 ++- .../index.js => fields_stats_grid/index.ts} | 2 +- .../file_datavisualizer_view.js | 1 - .../components/results_view/results_view.tsx | 26 +-- .../datavisualizer/index_based/_index.scss | 2 +- .../index_based/common/index.ts | 1 - .../components/expanded_row}/expanded_row.tsx | 15 +- .../components/expanded_row/index.ts | 7 + .../field_count_panel/field_count_panel.tsx | 99 ++------- .../content_types/boolean_content.tsx | 84 -------- .../content_types/geo_point_content.tsx | 41 ---- .../content_types/ip_content.tsx | 34 --- .../content_types/keyword_content.tsx | 29 --- .../content_types/number_content.tsx | 200 ------------------ .../content_types/other_content.tsx | 83 -------- .../content_types/text_content.tsx | 62 ------ .../_index.scss | 1 - .../content_types/document_count_content.tsx | 4 +- .../field_data_row/content_types/index.ts | 8 + .../content_types/not_in_docs_content.tsx | 0 .../document_count_chart.tsx | 2 - .../document_count_chart/index.ts | 0 .../examples_list/examples_list.tsx | 2 +- .../examples_list/index.ts | 0 .../loading_indicator/index.ts | 0 .../loading_indicator/loading_indicator.tsx | 0 .../top_values/_top_values.scss | 0 .../top_values/index.ts | 0 .../top_values/top_values.tsx | 0 .../components/search_panel/search_panel.tsx | 2 - .../datavisualizer/index_based/page.tsx | 38 +++- .../_field_data_row.scss} | 0 .../_index.scss | 5 + .../expanded_row_field_header.tsx | 0 .../expanded_row_field_header/index.ts | 0 .../components/field_count_stats/_index.scss | 3 + .../components/field_count_stats/index.ts | 12 ++ .../field_count_stats/metric_fields_count.tsx | 67 ++++++ .../field_count_stats/total_fields_count.tsx | 66 ++++++ .../field_data_expanded_row/_index.scss | 0 .../_number_content.scss | 0 .../boolean_content.tsx | 143 +++++++++++++ .../field_data_expanded_row}/date_content.tsx | 40 ++-- .../document_stats.tsx | 91 ++++++++ .../geo_point_content.tsx | 35 +++ .../field_data_expanded_row}/index.ts | 2 - .../field_data_expanded_row/ip_content.tsx | 38 ++++ .../keyword_content.tsx | 35 +++ .../number_content.tsx | 20 +- .../field_data_expanded_row/other_content.tsx | 22 ++ .../field_data_expanded_row/text_content.tsx | 64 ++++++ .../boolean_content_preview.tsx | 40 ++++ .../field_data_row/distinct_values.tsx | 0 .../field_data_row/document_stats.tsx | 4 +- .../components/field_data_row}/index.ts | 2 +- .../field_data_row/number_content_preview.tsx | 10 +- .../field_data_row/top_values_preview.tsx | 4 +- .../metric_distribution_chart/index.ts | 0 .../metric_distribution_chart.tsx | 55 +---- ...metric_distribution_chart_data_builder.tsx | 0 ...tric_distribution_chart_tooltip_header.tsx | 2 +- .../data_visualizer_stats_table.tsx} | 87 +++++--- .../datavisualizer/stats_table/hooks/index.ts | 7 + .../hooks/use_data_viz_chart_theme.ts | 54 +++++ .../datavisualizer/stats_table/index.ts | 7 + .../stats_table/types/field_data_row.ts | 11 + .../types}/field_vis_config.ts | 27 ++- .../datavisualizer/stats_table/types/index.ts | 15 ++ .../datavisualizer/stats_table/utils.ts | 37 ++++ .../formatters/round_to_decimal_place.ts | 3 +- .../translations/translations/ja-JP.json | 17 -- .../translations/translations/zh-CN.json | 17 -- .../data_visualizer/file_data_visualizer.ts | 149 ++++++++++++- .../files_to_import/artificial_server_log | 39 ++-- .../data_visualizer/index_data_visualizer.ts | 2 +- .../services/ml/data_visualizer_table.ts | 21 +- 98 files changed, 1721 insertions(+), 1199 deletions(-) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card/field_data_card.tsx => file_based/components/expanded_row/file_based_expanded_row.tsx} (52%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/number_content_preview.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/field_names_filter.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card => file_based/components/field_names_filter}/index.ts (77%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/field_types_filter.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/index.ts delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/create_fields.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/fields_stats_grid.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/filter_fields.ts rename x-pack/plugins/ml/public/application/datavisualizer/file_based/components/{fields_stats/get_field_names.js => fields_stats_grid/get_field_names.ts} (53%) rename x-pack/plugins/ml/public/application/datavisualizer/file_based/components/{fields_stats/index.js => fields_stats_grid/index.ts} (81%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => index_based/components/expanded_row}/expanded_row.tsx (75%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/index.ts delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx delete mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/_index.scss (55%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/content_types/document_count_content.tsx (90%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/index.ts rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/content_types/not_in_docs_content.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/document_count_chart/document_count_chart.tsx (98%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/document_count_chart/index.ts (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/examples_list/examples_list.tsx (93%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/examples_list/index.ts (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/loading_indicator/index.ts (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/loading_indicator/loading_indicator.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/top_values/_top_values.scss (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/top_values/index.ts (100%) rename x-pack/plugins/ml/public/application/datavisualizer/index_based/components/{field_data_card => field_data_row}/top_values/top_values.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card/_field_data_card.scss => stats_table/_field_data_row.scss} (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/_index.scss (87%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/expanded_row_field_header/expanded_row_field_header.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/expanded_row_field_header/index.ts (100%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/_index.scss create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/metric_fields_count.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/total_fields_count.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_expanded_row/_index.scss (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_expanded_row/_number_content.scss (100%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card/content_types => stats_table/components/field_data_expanded_row}/date_content.tsx (58%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card/content_types => stats_table/components/field_data_expanded_row}/index.ts (83%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_expanded_row/number_content.tsx (89%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_row/distinct_values.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_row/document_stats.tsx (86%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table/components/field_data_row}/index.ts (78%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_row/number_content_preview.tsx (91%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid => stats_table}/components/field_data_row/top_values_preview.tsx (89%) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card => stats_table/components}/metric_distribution_chart/index.ts (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card => stats_table/components}/metric_distribution_chart/metric_distribution_chart.tsx (60%) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card => stats_table/components}/metric_distribution_chart/metric_distribution_chart_data_builder.tsx (100%) rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/components/field_data_card => stats_table/components}/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx (95%) rename x-pack/plugins/ml/public/application/datavisualizer/{stats_datagrid/stats_datagrid.tsx => stats_table/data_visualizer_stats_table.tsx} (76%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/use_data_viz_chart_theme.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_data_row.ts rename x-pack/plugins/ml/public/application/datavisualizer/{index_based/common => stats_table/types}/field_vis_config.ts (69%) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/stats_table/utils.ts diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index 9dc3896e9be48..b1967cfe83f3c 100644 --- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; + export interface InputOverrides { [key: string]: string; } @@ -29,15 +31,27 @@ export interface FindFileStructureResponse { count: number; cardinality: number; top_hits: Array<{ count: number; value: any }>; + mean_value?: number; + median_value?: number; max_value?: number; min_value?: number; + earliest?: string; + latest?: string; }; }; sample_start: string; num_messages_analyzed: number; mappings: { - [fieldName: string]: { - type: string; + properties: { + [fieldName: string]: { + // including all possible Elasticsearch types + // since find_file_structure API can be enhanced to include new fields in the future + type: Exclude< + ES_FIELD_TYPES, + ES_FIELD_TYPES._ID | ES_FIELD_TYPES._INDEX | ES_FIELD_TYPES._SOURCE | ES_FIELD_TYPES._TYPE + >; + format?: string; + }; }; }; quote: string; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 3c70cf4c27b5d..3ff57fc622da4 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -42,11 +42,7 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState { [key: string]: any; } -export interface DataVisualizerIndexBasedAppState { - pageIndex: number; - pageSize: number; - sortField: string; - sortDirection: string; +export interface DataVisualizerIndexBasedAppState extends Omit { searchString?: Query['query']; searchQuery?: Query['query']; searchQueryLanguage?: SearchQueryLanguage; @@ -57,6 +53,13 @@ export interface DataVisualizerIndexBasedAppState { showAllFields?: boolean; showEmptyFields?: boolean; } + +export interface DataVisualizerFileBasedAppState extends Omit { + visibleFieldTypes?: string[]; + visibleFieldNames?: string[]; + showDistributions?: boolean; +} + export type MlGenericUrlState = MLPageState< | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss index e07c8a7b81692..756804a0e6aa0 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss @@ -22,6 +22,7 @@ .mlDataGridChart__legendBoolean { width: 100%; + min-width: $euiButtonMinWidth; td { text-align: center } } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx index bb52941f463fe..2ecbe0601816a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx @@ -20,7 +20,7 @@ import { NON_AGGREGATABLE } from './common'; export const hoveredRow$ = new BehaviorSubject(null); -const BAR_COLOR = euiPaletteColorBlind()[0]; +export const BAR_COLOR = euiPaletteColorBlind()[0]; const BAR_COLOR_BLUR = euiPaletteColorBlind({ rotations: 2 })[10]; const MAX_CHART_COLUMNS = 20; diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx index 0e98a23637f03..dec149cdec403 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx @@ -11,11 +11,15 @@ import { EuiText, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldTypeIcon } from '../field_type_icon'; -import { FieldVisConfig } from '../../datavisualizer/index_based/common'; import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; +import { + FieldVisConfig, + FileBasedFieldVisConfig, + isIndexBasedFieldVisConfig, +} from '../../datavisualizer/stats_table/types/field_vis_config'; interface Props { - card: FieldVisConfig; + card: FieldVisConfig | FileBasedFieldVisConfig; } export const FieldTitleBar: FC = ({ card }) => { @@ -30,13 +34,13 @@ export const FieldTitleBar: FC = ({ card }) => { if (card.fieldName === undefined) { classNames.push('document_count'); - } else if (card.isUnsupportedType === true) { + } else if (isIndexBasedFieldVisConfig(card) && card.isUnsupportedType === true) { classNames.push('type-other'); } else { classNames.push(card.type); } - if (card.isUnsupportedType !== true) { + if (isIndexBasedFieldVisConfig(card) && card.isUnsupportedType !== true) { // All the supported field types have aria labels. cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type)!); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index da8e272103f3d..d74ed4447cfe9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -7,7 +7,10 @@ import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui'; import { useCallback, useMemo } from 'react'; import { ListingPageUrlState } from '../../../../../../../common/types/common'; -import { DataVisualizerIndexBasedAppState } from '../../../../../../../common/types/ml_url_generator'; +import { + DataVisualizerFileBasedAppState, + DataVisualizerIndexBasedAppState, +} from '../../../../../../../common/types/ml_url_generator'; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -38,7 +41,10 @@ interface UseTableSettingsReturnValue { export function useTableSettings( items: TypeOfItem[], - pageState: ListingPageUrlState | DataVisualizerIndexBasedAppState, + pageState: + | ListingPageUrlState + | DataVisualizerIndexBasedAppState + | DataVisualizerFileBasedAppState, updatePageState: (update: Partial) => void ): UseTableSettingsReturnValue { const { pageIndex, pageSize, sortField, sortDirection } = pageState; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/_index.scss index 081f8b971432e..195eeea72baa0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/_index.scss @@ -1,3 +1,3 @@ @import 'file_based/index'; @import 'index_based/index'; -@import 'stats_datagrid/index'; +@import 'stats_table/index'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss index 42974d098bda4..a7c3926407ea0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss @@ -1,7 +1,6 @@ @import 'file_datavisualizer_view/index'; @import 'results_view/index'; @import 'analysis_summary/index'; -@import 'fields_stats/index'; @import 'about_panel/index'; @import 'import_summary/index'; @import 'experimental_badge/index'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx similarity index 52% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx index a568356a06d26..77f31ae9c2322 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/file_based_expanded_row.tsx @@ -4,45 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; -import React, { FC } from 'react'; - -import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; - -import { FieldVisConfig } from '../../common'; -import { FieldTitleBar } from '../../../../components/field_title_bar/index'; +import React from 'react'; import { BooleanContent, DateContent, GeoPointContent, IpContent, KeywordContent, - NotInDocsContent, - NumberContent, OtherContent, TextContent, -} from './content_types'; -import { LoadingIndicator } from './loading_indicator'; - -export interface FieldDataCardProps { - config: FieldVisConfig; -} + NumberContent, +} from '../../../stats_table/components/field_data_expanded_row'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config'; -export const FieldDataCard: FC = ({ config }) => { - const { fieldName, loading, type, existsInDocs } = config; +export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => { + const config = item; + const { type, fieldName } = config; function getCardContent() { - if (existsInDocs === false) { - return ; - } - switch (type) { case ML_JOB_FIELD_TYPES.NUMBER: - if (fieldName !== undefined) { - return ; - } else { - return null; - } + return ; case ML_JOB_FIELD_TYPES.BOOLEAN: return ; @@ -68,15 +51,11 @@ export const FieldDataCard: FC = ({ config }) => { } return ( - - -
- {loading === true ? : getCardContent()} -
-
+ {getCardContent()} +
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/index.ts new file mode 100644 index 0000000000000..0601f739ed81b --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/expanded_row/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FileBasedDataVisualizerExpandedRow } from './file_based_expanded_row'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/index.ts new file mode 100644 index 0000000000000..2a7eab9beb22f --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FileBasedNumberContentPreview } from './number_content_preview'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/number_content_preview.tsx new file mode 100644 index 0000000000000..de6d129e0b462 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_data_row/number_content_preview.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FileBasedFieldVisConfig } from '../../../stats_table/types'; + +export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => { + const stats = config.stats; + if ( + stats === undefined || + stats.min === undefined || + stats.median === undefined || + stats.max === undefined + ) + return null; + return ( + + + + + + + + + + + + + + + + + + + + {stats.min} + {stats.median} + {stats.max} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/field_names_filter.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/field_names_filter.tsx new file mode 100644 index 0000000000000..afc0a95e7f59b --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/field_names_filter.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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MultiSelectPicker } from '../../../../components/multi_select_picker'; +import type { + FileBasedFieldVisConfig, + FileBasedUnknownFieldVisConfig, +} from '../../../stats_table/types/field_vis_config'; + +interface Props { + fields: Array; + setVisibleFieldNames(q: string[]): void; + visibleFieldNames: string[]; +} + +export const DataVisualizerFieldNamesFilter: FC = ({ + fields, + setVisibleFieldNames, + visibleFieldNames, +}) => { + const fieldNameTitle = useMemo( + () => + i18n.translate('xpack.ml.dataVisualizer.fileBased.fieldNameSelect', { + defaultMessage: 'Field name', + }), + [] + ); + const options = useMemo( + () => fields.filter((d) => d.fieldName !== undefined).map((d) => ({ value: d.fieldName! })), + [fields] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/index.ts similarity index 77% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/index.ts index 9b7939c90c71d..1bd19e27d3b9f 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_names_filter/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { FieldDataCard, FieldDataCardProps } from './field_data_card'; +export { DataVisualizerFieldNamesFilter } from './field_names_filter'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/field_types_filter.tsx new file mode 100644 index 0000000000000..f52a588844cdf --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/field_types_filter.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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { MultiSelectPicker, Option } from '../../../../components/multi_select_picker'; +import type { + FileBasedFieldVisConfig, + FileBasedUnknownFieldVisConfig, +} from '../../../stats_table/types/field_vis_config'; +import { FieldTypeIcon } from '../../../../components/field_type_icon'; +import { ML_JOB_FIELD_TYPES_OPTIONS } from '../../../index_based/components/search_panel/field_type_filter'; + +interface Props { + fields: Array; + setVisibleFieldTypes(q: string[]): void; + visibleFieldTypes: string[]; +} + +export const DataVisualizerFieldTypesFilter: FC = ({ + fields, + setVisibleFieldTypes, + visibleFieldTypes, +}) => { + const fieldNameTitle = useMemo( + () => + i18n.translate('xpack.ml.dataVisualizer.fileBased.fieldTypeSelect', { + defaultMessage: 'Field type', + }), + [] + ); + + const options = useMemo(() => { + const fieldTypesTracker = new Set(); + const fieldTypes: Option[] = []; + fields.forEach(({ type }) => { + if ( + type !== undefined && + !fieldTypesTracker.has(type) && + ML_JOB_FIELD_TYPES_OPTIONS[type] !== undefined + ) { + const item = ML_JOB_FIELD_TYPES_OPTIONS[type]; + + fieldTypesTracker.add(type); + fieldTypes.push({ + value: type, + name: ( + + {item.name} + {type && ( + + + + )} + + ), + }); + } + }); + return fieldTypes; + }, [fields]); + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/index.ts new file mode 100644 index 0000000000000..1209fd94790c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/field_types_filter/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataVisualizerFieldTypesFilter } from './field_types_filter'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss deleted file mode 100644 index fe6a232f016a3..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'fields_stats'; -@import 'field_stats_card'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js deleted file mode 100644 index 2e9efa43f36bc..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js +++ /dev/null @@ -1,184 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FieldTypeIcon } from '../../../../components/field_type_icon'; -import { DisplayValue } from '../../../../components/display_value'; -import { getMLJobTypeAriaLabel } from '../../../../util/field_types_utils'; - -export function FieldStatsCard({ field }) { - let type = field.type; - if (type === 'double' || type === 'long') { - type = 'number'; - } - - const typeAriaLabel = getMLJobTypeAriaLabel(type); - const cardTitleAriaLabel = [field.name]; - if (typeAriaLabel) { - cardTitleAriaLabel.unshift(typeAriaLabel); - } - - return ( - -
-
- -
- {field.name} -
-
- -
- {field.count > 0 && ( - -
- - - - - - - - - - - - - - - - - - {field.median_value && ( - -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
-
- )} -
- - {field.top_hits && ( - - - -
-
- -
- {field.top_hits.map(({ count, value }) => { - const pcnt = Math.round((count / field.count) * 100 * 100) / 100; - return ( - - - - {value}  - - - - - - - - {pcnt}% - - - - ); - })} -
-
- )} -
- )} - {field.count === 0 && ( -
-
- -
-
- )} -
-
-
- ); -} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js deleted file mode 100644 index 785dd7db260fc..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js +++ /dev/null @@ -1,113 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; - -import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import { FieldStatsCard } from './field_stats_card'; -import { getFieldNames } from './get_field_names'; -import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; -import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; - -export class FieldsStats extends Component { - constructor(props) { - super(props); - - this.state = { - fields: [], - }; - } - - componentDidMount() { - this.setState({ - fields: createFields(this.props.results), - }); - } - - render() { - return ( -
- - {this.state.fields.map((f) => ( - - - - ))} - -
- ); - } -} - -function createFields(results) { - const { - mappings, - field_stats: fieldStats, - num_messages_analyzed: numMessagesAnalyzed, - timestamp_field: timestampField, - } = results; - - if (mappings && mappings.properties && fieldStats) { - const fieldNames = getFieldNames(results); - - return fieldNames.map((name) => { - if (fieldStats[name] !== undefined) { - const field = { name }; - const f = fieldStats[name]; - const m = mappings.properties[name]; - - // sometimes the timestamp field is not in the mappings, and so our - // collection of fields will be missing a time field with a type of date - if (name === timestampField && field.type === undefined) { - field.type = ML_JOB_FIELD_TYPES.DATE; - } - - if (f !== undefined) { - Object.assign(field, f); - } - - if (m !== undefined) { - field.type = m.type; - if (m.format !== undefined) { - field.format = m.format; - } - } - - const percent = (field.count / numMessagesAnalyzed) * 100; - field.percent = roundToDecimalPlace(percent); - - // round min, max, median, mean to 2dp. - if (field.median_value !== undefined) { - field.median_value = roundToDecimalPlace(field.median_value); - field.mean_value = roundToDecimalPlace(field.mean_value); - field.min_value = roundToDecimalPlace(field.min_value); - field.max_value = roundToDecimalPlace(field.max_value); - } - - return field; - } else { - // field is not in the field stats - // this could be the message field for a semi-structured log file or a - // field which the endpoint has not been able to work out any information for - const type = - mappings.properties[name] && mappings.properties[name].type === ML_JOB_FIELD_TYPES.TEXT - ? ML_JOB_FIELD_TYPES.TEXT - : ML_JOB_FIELD_TYPES.UNKNOWN; - - return { - name, - type, - mean_value: 0, - count: 0, - cardinality: 0, - percent: 0, - }; - } - }); - } - - return []; -} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/create_fields.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/create_fields.ts new file mode 100644 index 0000000000000..6313656fc09c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/create_fields.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; +import { getFieldNames, getSupportedFieldType } from './get_field_names'; +import { FileBasedFieldVisConfig } from '../../../stats_table/types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; + +export function createFields(results: FindFileStructureResponse) { + const { + mappings, + field_stats: fieldStats, + num_messages_analyzed: numMessagesAnalyzed, + timestamp_field: timestampField, + } = results; + + let numericFieldsCount = 0; + + if (mappings && mappings.properties && fieldStats) { + const fieldNames = getFieldNames(results); + + const items = fieldNames.map((name) => { + if (fieldStats[name] !== undefined) { + const field: FileBasedFieldVisConfig = { + fieldName: name, + type: ML_JOB_FIELD_TYPES.UNKNOWN, + }; + const f = fieldStats[name]; + const m = mappings.properties[name]; + + // sometimes the timestamp field is not in the mappings, and so our + // collection of fields will be missing a time field with a type of date + if (name === timestampField && field.type === ML_JOB_FIELD_TYPES.UNKNOWN) { + field.type = ML_JOB_FIELD_TYPES.DATE; + } + + if (m !== undefined) { + field.type = getSupportedFieldType(m.type); + if (field.type === ML_JOB_FIELD_TYPES.NUMBER) { + numericFieldsCount += 1; + } + if (m.format !== undefined) { + field.format = m.format; + } + } + + let _stats = {}; + + // round min, max, median, mean to 2dp. + if (f.median_value !== undefined) { + _stats = { + ..._stats, + median: roundToDecimalPlace(f.median_value), + mean: roundToDecimalPlace(f.mean_value), + min: roundToDecimalPlace(f.min_value), + max: roundToDecimalPlace(f.max_value), + }; + } + if (f.cardinality !== undefined) { + _stats = { + ..._stats, + cardinality: f.cardinality, + count: f.count, + sampleCount: numMessagesAnalyzed, + }; + } + + if (f.top_hits !== undefined) { + if (field.type === ML_JOB_FIELD_TYPES.TEXT) { + _stats = { + ..._stats, + examples: f.top_hits.map((hit) => hit.value), + }; + } else { + _stats = { + ..._stats, + topValues: f.top_hits.map((hit) => ({ key: hit.value, doc_count: hit.count })), + }; + } + } + + if (field.type === ML_JOB_FIELD_TYPES.DATE) { + _stats = { + ..._stats, + earliest: f.earliest, + latest: f.latest, + }; + } + + field.stats = _stats; + return field; + } else { + // field is not in the field stats + // this could be the message field for a semi-structured log file or a + // field which the endpoint has not been able to work out any information for + const type = + mappings.properties[name] && mappings.properties[name].type === ML_JOB_FIELD_TYPES.TEXT + ? ML_JOB_FIELD_TYPES.TEXT + : ML_JOB_FIELD_TYPES.UNKNOWN; + + return { + fieldName: name, + type, + stats: { + mean: 0, + count: 0, + sampleCount: numMessagesAnalyzed, + cardinality: 0, + }, + }; + } + }); + + return { + fields: items, + totalFieldsCount: items.length, + totalMetricFieldsCount: numericFieldsCount, + }; + } + + return { fields: [], totalFieldsCount: 0, totalMetricFieldsCount: 0 }; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/fields_stats_grid.tsx new file mode 100644 index 0000000000000..e2911653ab41a --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/fields_stats_grid.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, FC } from 'react'; +import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import type { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; +import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../../../stats_table'; +import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config'; +import { FileBasedDataVisualizerExpandedRow } from '../expanded_row'; + +import { DataVisualizerFieldNamesFilter } from '../field_names_filter'; +import { DataVisualizerFieldTypesFilter } from '../field_types_filter'; +import { createFields } from './create_fields'; +import { filterFields } from './filter_fields'; +import { usePageUrlState } from '../../../../util/url_state'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { + MetricFieldsCount, + TotalFieldsCount, +} from '../../../stats_table/components/field_count_stats'; +import type { DataVisualizerFileBasedAppState } from '../../../../../../common/types/ml_url_generator'; + +interface Props { + results: FindFileStructureResponse; +} +export const getDefaultDataVisualizerListState = (): Required => ({ + pageIndex: 0, + pageSize: 10, + sortField: 'fieldName', + sortDirection: 'asc', + visibleFieldTypes: [], + visibleFieldNames: [], + showDistributions: true, +}); + +function getItemIdToExpandedRowMap( + itemIds: string[], + items: FileBasedFieldVisConfig[] +): ItemIdToExpandedRowMap { + return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { + const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); + if (item !== undefined) { + m[fieldName] = ; + } + return m; + }, {} as ItemIdToExpandedRowMap); +} + +export const FieldsStatsGrid: FC = ({ results }) => { + const restorableDefaults = getDefaultDataVisualizerListState(); + const [ + dataVisualizerListState, + setDataVisualizerListState, + ] = usePageUrlState( + ML_PAGES.DATA_VISUALIZER_FILE, + restorableDefaults + ); + const visibleFieldTypes = + dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes; + const setVisibleFieldTypes = (values: string[]) => { + setDataVisualizerListState({ ...dataVisualizerListState, visibleFieldTypes: values }); + }; + + const visibleFieldNames = + dataVisualizerListState.visibleFieldNames ?? restorableDefaults.visibleFieldNames; + const setVisibleFieldNames = (values: string[]) => { + setDataVisualizerListState({ ...dataVisualizerListState, visibleFieldNames: values }); + }; + + const { fields, totalFieldsCount, totalMetricFieldsCount } = useMemo( + () => createFields(results), + [results, visibleFieldNames, visibleFieldTypes] + ); + const { filteredFields, visibleFieldsCount, visibleMetricsCount } = useMemo( + () => filterFields(fields, visibleFieldNames, visibleFieldTypes), + [results, visibleFieldNames, visibleFieldTypes] + ); + + const fieldsCountStats = { visibleFieldsCount, totalFieldsCount }; + const metricsStats = { visibleMetricsCount, totalMetricFieldsCount }; + + return ( +
+ + + + + + + + + + + + + + items={filteredFields} + pageState={dataVisualizerListState} + updatePageState={setDataVisualizerListState} + getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} + /> +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/filter_fields.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/filter_fields.ts new file mode 100644 index 0000000000000..9f3ec88507aef --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/filter_fields.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import type { + FileBasedFieldVisConfig, + FileBasedUnknownFieldVisConfig, +} from '../../../stats_table/types/field_vis_config'; + +export function filterFields( + fields: Array, + visibleFieldNames: string[], + visibleFieldTypes: string[] +) { + let items = fields; + + if (visibleFieldTypes && visibleFieldTypes.length > 0) { + items = items.filter( + (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 + ); + } + if (visibleFieldNames && visibleFieldNames.length > 0) { + items = items.filter((config) => { + return visibleFieldNames.findIndex((field) => field === config.fieldName) > -1; + }); + } + + return { + filteredFields: items, + visibleFieldsCount: items.length, + visibleMetricsCount: items.filter((d) => d.type === ML_JOB_FIELD_TYPES.NUMBER).length, + }; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/get_field_names.ts similarity index 53% rename from x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/get_field_names.ts index c423dc3c63e39..e2f73505f6cd2 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/get_field_names.ts @@ -5,8 +5,11 @@ */ import { difference } from 'lodash'; - -export function getFieldNames(results) { +import type { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; +import { MlJobFieldType } from '../../../../../../common/types/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; +export function getFieldNames(results: FindFileStructureResponse) { const { mappings, field_stats: fieldStats, column_names: columnNames } = results; // if columnNames exists (i.e delimited) use it for the field list @@ -29,3 +32,24 @@ export function getFieldNames(results) { } return tempFields; } + +export function getSupportedFieldType(type: string): MlJobFieldType { + switch (type) { + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SHORT: + case ES_FIELD_TYPES.UNSIGNED_LONG: + return ML_JOB_FIELD_TYPES.NUMBER; + + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + return ML_JOB_FIELD_TYPES.DATE; + + default: + return type as MlJobFieldType; + } +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/index.ts similarity index 81% rename from x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/index.ts index 760a206dfa6ba..693d9578644ac 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats_grid/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { FieldsStats } from './fields_stats'; +export { FieldsStatsGrid } from './fields_stats_grid'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index 56b81e36f1e92..a376e64b78216 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -228,7 +228,6 @@ export class FileDataVisualizerView extends Component { }; setOverrides = (overrides) => { - console.log('setOverrides', overrides); this.setState( { loading: true, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.tsx index f9de03c119d28..12e60ea491421 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; @@ -15,7 +14,6 @@ import { EuiPageBody, EuiPageContentHeader, EuiPanel, - EuiTabbedContent, EuiSpacer, EuiTitle, EuiFlexGroup, @@ -25,8 +23,7 @@ import { FindFileStructureResponse } from '../../../../../../common/types/file_d import { FileContents } from '../file_contents'; import { AnalysisSummary } from '../analysis_summary'; -// @ts-ignore -import { FieldsStats } from '../fields_stats'; +import { FieldsStatsGrid } from '../fields_stats_grid'; interface Props { data: string; @@ -45,16 +42,6 @@ export const ResultsView: FC = ({ showExplanationFlyout, disableButtons, }) => { - const tabs = [ - { - id: 'file-stats', - name: i18n.translate('xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', { - defaultMessage: 'File stats', - }), - content: , - }, - ]; - return ( @@ -103,7 +90,16 @@ export const ResultsView: FC = ({ - {}} /> + +

+ +

+
+ +
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/index_based/_index.scss index 53943f8ada6e8..95a523753dfca 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/_index.scss @@ -1 +1 @@ -@import 'components/field_data_card/index'; +@import 'components/field_data_row/index'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts index 50278c300d103..35bec2eb66379 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { FieldVisConfig } from './field_vis_config'; export { FieldHistogramRequestConfig, FieldRequestConfig } from './request'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx similarity index 75% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx index cb83d6db83ed3..7018f73ff6c32 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx @@ -6,22 +6,23 @@ import React from 'react'; -import { FieldVisConfig } from '../index_based/common'; +import { FieldVisConfig } from '../../../stats_table/types'; import { BooleanContent, DateContent, GeoPointContent, IpContent, KeywordContent, - NotInDocsContent, + NumberContent, OtherContent, TextContent, -} from '../index_based/components/field_data_card/content_types'; -import { NumberContent } from './components/field_data_expanded_row/number_content'; -import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; -import { LoadingIndicator } from '../index_based/components/field_data_card/loading_indicator'; +} from '../../../stats_table/components/field_data_expanded_row'; -export const DataVisualizerFieldExpandedRow = ({ item }: { item: FieldVisConfig }) => { +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { LoadingIndicator } from '../field_data_row/loading_indicator'; +import { NotInDocsContent } from '../field_data_row/content_types'; + +export const IndexBasedDataVisualizerExpandedRow = ({ item }: { item: FieldVisConfig }) => { const config = item; const { loading, type, existsInDocs, fieldName } = config; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/index.ts new file mode 100644 index 0000000000000..3b393d96c97e3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { IndexBasedDataVisualizerExpandedRow } from './expanded_row'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx index 61bf244fbbcdb..1996ca585147b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiSwitch, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; +import { + MetricFieldsCount, + TotalFieldsCount, +} from '../../../stats_table/components/field_count_stats'; +import type { + TotalFieldsCountProps, + MetricFieldsCountProps, +} from '../../../stats_table/components/field_count_stats'; -interface Props { - metricsStats?: { - visibleMetricFields: number; - totalMetricFields: number; - }; - fieldsCountStats?: { - visibleFieldsCount: number; - totalFieldsCount: number; - }; +interface Props extends TotalFieldsCountProps, MetricFieldsCountProps { showEmptyFields: boolean; toggleShowEmptyFields: () => void; } @@ -33,83 +33,8 @@ export const FieldCountPanel: FC = ({ style={{ marginLeft: 4 }} data-test-subj="mlDataVisualizerFieldCountPanel" > - {fieldsCountStats && ( - - - -
- -
-
-
- - - - {fieldsCountStats.visibleFieldsCount} - - - - - - - -
- )} - - {metricsStats && ( - - - -
- -
-
-
- - - {metricsStats.visibleMetricFields} - - - - - - - -
- )} - + + = 0.1) { - return `${value}%`; - } else { - return '< 0.1%'; - } -} - -export const BooleanContent: FC = ({ config }) => { - const { stats } = config; - if (stats === undefined) return null; - const { count, trueCount, falseCount } = stats; - if (count === undefined || trueCount === undefined || falseCount === undefined) return null; - - return ( -
- - - - - - - - - -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx deleted file mode 100644 index 0c10cf1e6adcf..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; - -import { FieldDataCardProps } from '../field_data_card'; -import { ExamplesList } from '../examples_list'; - -export const GeoPointContent: FC = ({ config }) => { - // TODO - adjust server-side query to get examples using: - - // GET /filebeat-apache-2019.01.30/_search - // { - // "size":10, - // "_source": false, - // "docvalue_fields": ["source.geo.location"], - // "query": { - // "bool":{ - // "must":[ - // { - // "exists":{ - // "field":"source.geo.location" - // } - // } - // ] - // } - // } - // } - - const { stats } = config; - if (stats?.examples === undefined) return null; - - return ( -
- -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx deleted file mode 100644 index 4b54e86cdc495..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FieldDataCardProps } from '../field_data_card'; -import { TopValues } from '../top_values'; -import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; - -export const IpContent: FC = ({ config }) => { - const { stats, fieldFormat } = config; - if (stats === undefined) return null; - const { count, sampleCount, cardinality } = stats; - if (count === undefined || sampleCount === undefined || cardinality === undefined) return null; - - return ( -
- - - - - -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx deleted file mode 100644 index 18c4fb190a125..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx +++ /dev/null @@ -1,29 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldDataCardProps } from '../field_data_card'; -import { TopValues } from '../top_values'; -import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; - -export const KeywordContent: FC = ({ config }) => { - const { stats, fieldFormat } = config; - - return ( -
- - - - - -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx deleted file mode 100644 index 782880105da20..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx +++ /dev/null @@ -1,200 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC, Fragment, useEffect, useState } from 'react'; -import { - EuiButtonGroup, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { FieldDataCardProps } from '../field_data_card'; -import { DisplayValue } from '../../../../../components/display_value'; -import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format'; -import { numberAsOrdinal } from '../../../../../formatters/number_as_ordinal'; -import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; -import { - MetricDistributionChart, - MetricDistributionChartData, - buildChartDataFromStats, -} from '../metric_distribution_chart'; -import { TopValues } from '../top_values'; - -const DETAILS_MODE = { - DISTRIBUTION: 'distribution', - TOP_VALUES: 'top_values', -} as const; - -type DetailsModeType = typeof DETAILS_MODE[keyof typeof DETAILS_MODE]; - -const METRIC_DISTRIBUTION_CHART_WIDTH = 325; -const METRIC_DISTRIBUTION_CHART_HEIGHT = 210; -const DEFAULT_TOP_VALUES_THRESHOLD = 100; - -export const NumberContent: FC = ({ config }) => { - const { stats, fieldFormat } = config; - - useEffect(() => { - const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); - setDistributionChartData(chartData); - }, []); - const [detailsMode, setDetailsMode] = useState( - stats?.cardinality ?? 0 <= DEFAULT_TOP_VALUES_THRESHOLD - ? DETAILS_MODE.TOP_VALUES - : DETAILS_MODE.DISTRIBUTION - ); - const defaultChartData: MetricDistributionChartData[] = []; - const [distributionChartData, setDistributionChartData] = useState(defaultChartData); - - if (stats === undefined) return null; - const { count, sampleCount, cardinality, min, median, max, distribution } = stats; - if (count === undefined || sampleCount === undefined) return null; - - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); - - const detailsOptions = [ - { - id: DETAILS_MODE.TOP_VALUES, - label: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel', { - defaultMessage: 'Top values', - }), - }, - { - id: DETAILS_MODE.DISTRIBUTION, - label: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel', { - defaultMessage: 'Distribution', - }), - }, - ]; - - return ( -
-
- - -   - - -
- -
- - -   - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setDetailsMode(optionId as DetailsModeType)} - legend={i18n.translate( - 'xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel', - { - defaultMessage: 'Select display option for metric details', - } - )} - data-test-subj="mlFieldDataCardDetailsSelect" - isFullWidth={true} - buttonSize="compressed" - /> - - {distribution && detailsMode === DETAILS_MODE.DISTRIBUTION && ( - - - - - - - - - - - - - - - )} - {detailsMode === DETAILS_MODE.TOP_VALUES && ( - - - - - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx deleted file mode 100644 index 065d7d40c23e9..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx +++ /dev/null @@ -1,83 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC, Fragment } from 'react'; -import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FieldDataCardProps } from '../field_data_card'; -import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; -import { ExamplesList } from '../examples_list'; - -export const OtherContent: FC = ({ config }) => { - const { stats, type, aggregatable } = config; - if (stats === undefined) return null; - - const { count, sampleCount, cardinality, examples } = stats; - if ( - count === undefined || - sampleCount === undefined || - cardinality === undefined || - examples === undefined - ) - return null; - - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); - - return ( -
-
- - - -
- {aggregatable === true && ( - - -
- - -   - - -
- - - -
- - -   - - -
-
- )} - - -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx deleted file mode 100644 index d54d2237c6603..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC, Fragment } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { FieldDataCardProps } from '../field_data_card'; -import { ExamplesList } from '../examples_list'; - -export const TextContent: FC = ({ config }) => { - const { stats } = config; - if (stats === undefined) return null; - - const { examples } = stats; - if (examples === undefined) return null; - - const numExamples = examples.length; - - return ( -
- {numExamples > 0 && } - {numExamples === 0 && ( - - - - _source, - }} - /> - - - - copy_to, - sourceParam: _source, - includesParam: includes, - excludesParam: excludes, - }} - /> - - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/_index.scss similarity index 55% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/_index.scss index e7c155d2554ba..38327dc51bd97 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/_index.scss @@ -1,2 +1 @@ -@import 'field_data_card'; @import 'top_values/top_values'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx similarity index 90% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx index e7b9604c1c06e..2df1b6799214d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx @@ -6,11 +6,11 @@ import React, { FC } from 'react'; -import type { FieldDataCardProps } from '../field_data_card'; +import type { FieldDataRowProps } from '../../../../stats_table/types/field_data_row'; import { DocumentCountChart, DocumentCountChartPoint } from '../document_count_chart'; import { TotalCountHeader } from '../../total_count_header'; -export interface Props extends FieldDataCardProps { +export interface Props extends FieldDataRowProps { totalCount: number; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/index.ts new file mode 100644 index 0000000000000..dd1f38b4a1349 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DocumentCountContent } from './document_count_content'; +export { NotInDocsContent } from './not_in_docs_content'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/not_in_docs_content.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/not_in_docs_content.tsx diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx index 6a02cb6acebd4..f0ac9b7e67d32 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx @@ -25,7 +25,6 @@ export interface DocumentCountChartPoint { interface Props { width?: number; - height?: number; chartPoints: DocumentCountChartPoint[]; timeRangeEarliest: number; timeRangeLatest: number; @@ -35,7 +34,6 @@ const SPEC_ID = 'document_count'; export const DocumentCountChart: FC = ({ width, - height, chartPoints, timeRangeEarliest, timeRangeLatest, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx similarity index 93% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx index 5591e6f9b5417..1e8f7586258d5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/examples_list.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { EuiListGroup, EuiListGroupItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; +import { ExpandedRowFieldHeader } from '../../../../stats_table/components/expanded_row_field_header'; interface Props { examples: Array; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/examples_list/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/loading_indicator/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/loading_indicator/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/loading_indicator/loading_indicator.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/loading_indicator/loading_indicator.tsx diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/_top_values.scss b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/_top_values.scss similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/_top_values.scss rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/_top_values.scss diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/top_values/top_values.tsx diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index af3c1a0e7c16c..8064e08c9f0f9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -157,8 +157,6 @@ export const SearchPanel: FC = ({ setVisibleFieldTypes={setVisibleFieldTypes} visibleFieldTypes={visibleFieldTypes} /> - - ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 9819bf451e425..e5b243d524034 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -45,17 +45,23 @@ import { getToastNotifications } from '../../util/dependency_cache'; import { usePageUrlState, useUrlState } from '../../util/url_state'; import { ActionsPanel } from './components/actions_panel'; import { SearchPanel } from './components/search_panel'; -import { DocumentCountContent } from './components/field_data_card/content_types/document_count_content'; -import { DataVisualizerDataGrid } from '../stats_datagrid'; +import { DocumentCountContent } from './components/field_data_row/content_types/document_count_content'; +import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table'; import { FieldCountPanel } from './components/field_count_panel'; import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; import { DataLoader } from './data_loader'; -import type { FieldRequestConfig, FieldVisConfig } from './common'; +import type { FieldRequestConfig } from './common'; import type { DataVisualizerIndexBasedAppState } from '../../../../common/types/ml_url_generator'; import type { OverallStats } from '../../../../common/types/datavisualizer'; import { MlJobFieldType } from '../../../../common/types/field_types'; import { HelpMenu } from '../../components/help_menu'; import { useMlKibana } from '../../contexts/kibana'; +import { IndexBasedDataVisualizerExpandedRow } from './components/expanded_row'; +import { FieldVisConfig } from '../stats_table/types'; +import type { + MetricFieldsStats, + TotalFieldsStats, +} from '../stats_table/components/field_count_stats'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -106,6 +112,19 @@ export const getDefaultDataVisualizerListState = (): Required { + const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); + if (item !== undefined) { + m[fieldName] = ; + } + return m; + }, {} as ItemIdToExpandedRowMap); +} + export const Page: FC = () => { const mlContext = useMlContext(); const restorableDefaults = getDefaultDataVisualizerListState(); @@ -228,9 +247,7 @@ export const Page: FC = () => { const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); - const [metricsStats, setMetricsStats] = useState< - undefined | { visibleMetricFields: number; totalMetricFields: number } - >(); + const [metricsStats, setMetricsStats] = useState(); const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); @@ -537,8 +554,8 @@ export const Page: FC = () => { }); setMetricsStats({ - totalMetricFields: allMetricFields.length, - visibleMetricFields: metricFieldsToShow.length, + totalMetricFieldsCount: allMetricFields.length, + visibleMetricsCount: metricFieldsToShow.length, }); setMetricConfigs(configs); } @@ -642,7 +659,7 @@ export const Page: FC = () => { return combinedConfigs; }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); - const fieldsCountStats = useMemo(() => { + const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => { let _visibleFieldsCount = 0; let _totalFieldsCount = 0; Object.keys(overallStats).forEach((key) => { @@ -736,10 +753,11 @@ export const Page: FC = () => { metricsStats={metricsStats} /> - items={configs} pageState={dataVisualizerListState} updatePageState={setDataVisualizerListState} + getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} /> diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/_field_data_row.scss diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss similarity index 87% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss index e9ecfc8b19100..6e7e66db9e03a 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/_index.scss @@ -1,4 +1,5 @@ @import 'components/field_data_expanded_row/number_content'; +@import 'components/field_count_stats/index'; .mlDataVisualizerFieldExpandedRow { padding-left: $euiSize * 4; @@ -35,6 +36,7 @@ } } .mlDataVisualizerSummaryTable { + max-width: 350px; .euiTableRow > .euiTableRowCell { border-bottom: 0; } @@ -42,4 +44,7 @@ display: none; } } + .mlDataVisualizerSummaryTableWrapper { + max-width: 350px; + } } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/expanded_row_field_header.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/expanded_row_field_header.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/expanded_row_field_header/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/expanded_row_field_header/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/_index.scss new file mode 100644 index 0000000000000..7154d0da2c09c --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/_index.scss @@ -0,0 +1,3 @@ +.mlDataVisualizerFieldCountContainer { + max-width: 300px; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/index.ts new file mode 100644 index 0000000000000..15c9c92f51cfb --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TotalFieldsCount, TotalFieldsCountProps, TotalFieldsStats } from './total_fields_count'; +export { + MetricFieldsCount, + MetricFieldsCountProps, + MetricFieldsStats, +} from './metric_fields_count'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/metric_fields_count.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/metric_fields_count.tsx new file mode 100644 index 0000000000000..327a3e611296c --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/metric_fields_count.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC } from 'react'; + +export interface MetricFieldsStats { + visibleMetricsCount: number; + totalMetricFieldsCount: number; +} +export interface MetricFieldsCountProps { + metricsStats?: MetricFieldsStats; +} + +export const MetricFieldsCount: FC = ({ metricsStats }) => { + if ( + !metricsStats || + metricsStats.visibleMetricsCount === undefined || + metricsStats.totalMetricFieldsCount === undefined + ) + return null; + return ( + <> + {metricsStats && ( + + + +
+ +
+
+
+ + + {metricsStats.visibleMetricsCount} + + + + + + + +
+ )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/total_fields_count.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/total_fields_count.tsx new file mode 100644 index 0000000000000..c90770dbf8c57 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_count_stats/total_fields_count.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC } from 'react'; + +export interface TotalFieldsStats { + visibleFieldsCount: number; + totalFieldsCount: number; +} + +export interface TotalFieldsCountProps { + fieldsCountStats?: TotalFieldsStats; +} + +export const TotalFieldsCount: FC = ({ fieldsCountStats }) => { + if ( + !fieldsCountStats || + fieldsCountStats.visibleFieldsCount === undefined || + fieldsCountStats.totalFieldsCount === undefined + ) + return null; + + return ( + + + +
+ +
+
+
+ + + + {fieldsCountStats.visibleFieldsCount} + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/_index.scss rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_index.scss diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/_number_content.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_number_content.scss similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/_number_content.scss rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/_number_content.scss diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx new file mode 100644 index 0000000000000..a75920dd09b34 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/boolean_content.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactNode, useMemo } from 'react'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { Axis, BarSeries, Chart, Settings } from '@elastic/charts'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { getTFPercentage } from '../../utils'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +import { useDataVizChartTheme } from '../../hooks'; +import { DocumentStatsTable } from './document_stats'; + +function getPercentLabel(value: number): string { + if (value === 0) { + return '0%'; + } + if (value >= 0.1) { + return `${roundToDecimalPlace(value)}%`; + } else { + return '< 0.1%'; + } +} + +function getFormattedValue(value: number, totalCount: number): string { + const percentage = (value / totalCount) * 100; + return `${value} (${getPercentLabel(percentage)})`; +} + +const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 100; + +export const BooleanContent: FC = ({ config }) => { + const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; + const formattedPercentages = useMemo(() => getTFPercentage(config), [config]); + const theme = useDataVizChartTheme(); + if (!formattedPercentages) return null; + + const { trueCount, falseCount, count } = formattedPercentages; + const summaryTableItems = [ + { + function: 'true', + display: ( + + ), + value: getFormattedValue(trueCount, count), + }, + { + function: 'false', + display: ( + + ), + value: getFormattedValue(falseCount, count), + }, + ]; + const summaryTableColumns = [ + { + name: '', + render: (summaryItem: { display: ReactNode }) => summaryItem.display, + width: '75px', + }, + { + field: 'value', + name: '', + render: (v: string) => {v}, + }, + ]; + + const summaryTableTitle = i18n.translate( + 'xpack.ml.fieldDataCardExpandedRow.booleanContent.summaryTableTitle', + { + defaultMessage: 'Summary', + } + ); + + return ( + + + + + {summaryTableTitle} + + + + + + + + + + + getFormattedValue(d, count)} + /> + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx similarity index 58% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx index 7651d20249c93..8d122df628381 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/date_content.tsx @@ -5,14 +5,15 @@ */ import React, { FC, ReactNode } from 'react'; -import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { FieldDataCardProps } from '../field_data_card'; -import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { DocumentStatsTable } from './document_stats'; const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS'; interface SummaryTableItem { function: string; @@ -20,7 +21,7 @@ interface SummaryTableItem { value: number | string | undefined | null; } -export const DateContent: FC = ({ config }) => { +export const DateContent: FC = ({ config }) => { const { stats } = config; if (stats === undefined) return null; @@ -38,7 +39,7 @@ export const DateContent: FC = ({ config }) => { defaultMessage="earliest" /> ), - value: formatDate(earliest, TIME_FORMAT), + value: typeof earliest === 'string' ? earliest : formatDate(earliest, TIME_FORMAT), }, { function: 'latest', @@ -48,7 +49,7 @@ export const DateContent: FC = ({ config }) => { defaultMessage="latest" /> ), - value: formatDate(latest, TIME_FORMAT), + value: typeof latest === 'string' ? latest : formatDate(latest, TIME_FORMAT), }, ]; const summaryTableColumns = [ @@ -65,17 +66,20 @@ export const DateContent: FC = ({ config }) => { ]; return ( - <> - {summaryTableTitle} - - className={'mlDataVisualizerSummaryTable'} - data-test-subj={'mlDateSummaryTable'} - compressed - items={summaryTableItems} - columns={summaryTableColumns} - tableCaption={summaryTableTitle} - tableLayout="auto" - /> - + + + + {summaryTableTitle} + + className={'mlDataVisualizerSummaryTable'} + data-test-subj={'mlDateSummaryTable'} + compressed + items={summaryTableItems} + columns={summaryTableColumns} + tableCaption={summaryTableTitle} + tableLayout="auto" + /> + + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx new file mode 100644 index 0000000000000..177ac722166f7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/document_stats.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable, EuiFlexItem } from '@elastic/eui'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { FieldDataRowProps } from '../../types'; + +const metaTableColumns = [ + { + name: '', + render: (metaItem: { display: ReactNode }) => metaItem.display, + width: '75px', + }, + { + field: 'value', + name: '', + render: (v: string) => {v}, + }, +]; + +const metaTableTitle = i18n.translate( + 'xpack.ml.fieldDataCardExpandedRow.documentStatsTable.metaTableTitle', + { + defaultMessage: 'Documents stats', + } +); + +export const DocumentStatsTable: FC = ({ config }) => { + if ( + config?.stats === undefined || + config.stats.cardinality === undefined || + config.stats.count === undefined || + config.stats.sampleCount === undefined + ) + return null; + const { cardinality, count, sampleCount } = config.stats; + const metaTableItems = [ + { + function: 'count', + display: ( + + ), + value: count, + }, + { + function: 'percentage', + display: ( + + ), + value: `${(count / sampleCount) * 100}%`, + }, + { + function: 'distinctValues', + display: ( + + ), + value: cardinality, + }, + ]; + + return ( + + {metaTableTitle} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx new file mode 100644 index 0000000000000..993c7a94f5e06 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/geo_point_content.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; +import { DocumentStatsTable } from './document_stats'; +import { TopValues } from '../../../index_based/components/field_data_row/top_values'; + +export const GeoPointContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined || (stats?.examples === undefined && stats?.topValues === undefined)) + return null; + + return ( + + + {Array.isArray(stats.examples) && ( + + + + )} + {Array.isArray(stats.topValues) && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts similarity index 83% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts index 230be246eb4eb..c6cd50f6bc2e9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/index.ts @@ -6,11 +6,9 @@ export { BooleanContent } from './boolean_content'; export { DateContent } from './date_content'; -export { DocumentCountContent } from './document_count_content'; export { GeoPointContent } from './geo_point_content'; export { KeywordContent } from './keyword_content'; export { IpContent } from './ip_content'; -export { NotInDocsContent } from './not_in_docs_content'; export { NumberContent } from './number_content'; export { OtherContent } from './other_content'; export { TextContent } from './text_content'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx new file mode 100644 index 0000000000000..79492bb44a2dc --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/ip_content.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { TopValues } from '../../../index_based/components/field_data_row/top_values'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { DocumentStatsTable } from './document_stats'; + +export const IpContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) return null; + const { count, sampleCount, cardinality } = stats; + if (count === undefined || sampleCount === undefined || cardinality === undefined) return null; + const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx new file mode 100644 index 0000000000000..634f5b55513a3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { TopValues } from '../../../index_based/components/field_data_row/top_values'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { DocumentStatsTable } from './document_stats'; + +export const KeywordContent: FC = ({ config }) => { + const { stats } = config; + const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; + + return ( + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx similarity index 89% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx index c3ba6d23f6baf..d05de26d3c5d4 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/number_content.tsx @@ -9,17 +9,17 @@ import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui' import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - -import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; +import type { FieldDataRowProps } from '../../types/field_data_row'; import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; import { numberAsOrdinal } from '../../../../formatters/number_as_ordinal'; import { MetricDistributionChart, MetricDistributionChartData, buildChartDataFromStats, -} from '../../../index_based/components/field_data_card/metric_distribution_chart'; -import { TopValues } from '../../../index_based/components/field_data_card/top_values'; +} from '../metric_distribution_chart'; +import { TopValues } from '../../../index_based/components/field_data_row/top_values'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; +import { DocumentStatsTable } from './document_stats'; const METRIC_DISTRIBUTION_CHART_WIDTH = 325; const METRIC_DISTRIBUTION_CHART_HEIGHT = 200; @@ -30,8 +30,8 @@ interface SummaryTableItem { value: number | string | undefined | null; } -export const NumberContent: FC = ({ config }) => { - const { stats, fieldFormat } = config; +export const NumberContent: FC = ({ config }) => { + const { stats } = config; useEffect(() => { const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); @@ -43,6 +43,7 @@ export const NumberContent: FC = ({ config }) => { if (stats === undefined) return null; const { min, median, max, distribution } = stats; + const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; const summaryTableItems = [ { @@ -96,8 +97,9 @@ export const NumberContent: FC = ({ config }) => { } ); return ( - - + + + {summaryTableTitle} className={'mlDataVisualizerSummaryTable'} @@ -105,8 +107,10 @@ export const NumberContent: FC = ({ config }) => { items={summaryTableItems} columns={summaryTableColumns} tableCaption={summaryTableTitle} + data-test-subj={'mlNumberSummaryTable'} /> + {stats && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx new file mode 100644 index 0000000000000..a6d7398990cd3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/other_content.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; +import { DocumentStatsTable } from './document_stats'; + +export const OtherContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) return null; + return ( + + + {Array.isArray(stats.examples) && } + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx new file mode 100644 index 0000000000000..55639ecc5761f --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_expanded_row/text_content.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; + +export const TextContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) return null; + + const { examples } = stats; + if (examples === undefined) return null; + + const numExamples = examples.length; + + return ( + + + {numExamples > 0 && } + {numExamples === 0 && ( + + + + _source, + }} + /> + + + + copy_to, + sourceParam: _source, + includesParam: includes, + excludesParam: excludes, + }} + /> + + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx new file mode 100644 index 0000000000000..e1a8a2f0dbeb6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FC, useMemo } from 'react'; +import { EuiDataGridColumn } from '@elastic/eui'; +import { FieldDataRowProps } from '../../types'; +import { getTFPercentage } from '../../utils'; +import { ColumnChart } from '../../../../components/data_grid/column_chart'; +import { OrdinalChartData } from '../../../../components/data_grid/use_column_chart'; + +export const BooleanContentPreview: FC = ({ config }) => { + const chartData = useMemo(() => { + const results = getTFPercentage(config); + if (results) { + const data = [ + { key: 'true', key_as_string: 'true', doc_count: results.trueCount }, + { key: 'false', key_as_string: 'false', doc_count: results.falseCount }, + ]; + return { id: config.fieldName, cardinality: 2, data, type: 'boolean' } as OrdinalChartData; + } + }, [config]); + if (!chartData || config.fieldName === undefined) return null; + + const columnType: EuiDataGridColumn = { + id: config.fieldName, + schema: undefined, + }; + const dataTestSubj = `mlDataGridChart-${config.fieldName}`; + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/distinct_values.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/distinct_values.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/distinct_values.tsx diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx similarity index 86% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx index 9421b7d9f51e7..9c89d74fa751b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/document_stats.tsx @@ -7,10 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import React from 'react'; -import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; +import type { FieldDataRowProps } from '../../types/field_data_row'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; -export const DocumentStat = ({ config }: FieldDataCardProps) => { +export const DocumentStat = ({ config }: FieldDataRowProps) => { const { stats } = config; if (stats === undefined) return null; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/index.ts similarity index 78% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/index.ts index 35a785e3cba67..2f1a958e657fd 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { DataVisualizerDataGrid } from './stats_datagrid'; +export { BooleanContentPreview } from './boolean_content_preview'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx similarity index 91% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx index 13070aeac6a4e..3a84ae644cb4e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/number_content_preview.tsx @@ -7,18 +7,22 @@ import React, { FC, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import classNames from 'classnames'; -import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; import { MetricDistributionChart, MetricDistributionChartData, buildChartDataFromStats, -} from '../../../index_based/components/field_data_card/metric_distribution_chart'; +} from '../metric_distribution_chart'; import { formatSingleValue } from '../../../../formatters/format_value'; +import { FieldVisConfig } from '../../types'; const METRIC_DISTRIBUTION_CHART_WIDTH = 150; const METRIC_DISTRIBUTION_CHART_HEIGHT = 80; -export const NumberContentPreview: FC = ({ config }) => { +export interface NumberContentPreviewProps { + config: FieldVisConfig; +} + +export const IndexBasedNumberContentPreview: FC = ({ config }) => { const { stats, fieldFormat, fieldName } = config; const defaultChartData: MetricDistributionChartData[] = []; const [distributionChartData, setDistributionChartData] = useState(defaultChartData); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx similarity index 89% rename from x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx index 52607ee71f25b..3ae9147e475b1 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx @@ -6,12 +6,12 @@ import React, { FC } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; -import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; +import type { FieldDataRowProps } from '../../types/field_data_row'; import { ColumnChart } from '../../../../components/data_grid/column_chart'; import { ChartData } from '../../../../components/data_grid'; import { OrdinalDataItem } from '../../../../components/data_grid/use_column_chart'; -export const TopValuesPreview: FC = ({ config }) => { +export const TopValuesPreview: FC = ({ config }) => { const { stats } = config; if (stats === undefined) return null; const { topValues, cardinality } = stats; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/metric_distribution_chart/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/metric_distribution_chart/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx similarity index 60% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx index 1abc497438079..786ebd9866cca 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx @@ -19,13 +19,10 @@ import { TooltipValueFormatter, } from '@elastic/charts'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; - import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header'; -import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; -import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format'; -import type { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service'; +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; +import type { ChartTooltipValue } from '../../../../components/chart_tooltip/chart_tooltip_service'; +import { useDataVizChartTheme } from '../../hooks'; export interface MetricDistributionChartData { x: number; @@ -59,9 +56,7 @@ export const MetricDistributionChart: FC = ({ defaultMessage: 'distribution', }); - const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); - const themeName = IS_DARK_THEME ? darkTheme : lightTheme; - const AREA_SERIES_COLOR = themeName.euiColorVis0; + const theme = useDataVizChartTheme(); const headerFormatter: TooltipValueFormatter = (tooltipData: ChartTooltipValue) => { const xValue = tooltipData.value; @@ -81,47 +76,7 @@ export const MetricDistributionChart: FC = ({ return (
- + ) => void; -} +const FIELD_NAME = 'fieldName'; export type ItemIdToExpandedRowMap = Record; -function getItemIdToExpandedRowMap( - itemIds: string[], - items: FieldVisConfig[] -): ItemIdToExpandedRowMap { - return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { - const item = items.find((fieldVisConfig) => fieldVisConfig[FIELD_NAME] === fieldName); - if (item !== undefined) { - m[fieldName] = ; - } - return m; - }, {} as ItemIdToExpandedRowMap); +type DataVisualizerTableItem = FieldVisConfig | FileBasedFieldVisConfig; +interface DataVisualizerTableProps { + items: T[]; + pageState: DataVisualizerIndexBasedAppState | DataVisualizerFileBasedAppState; + updatePageState: ( + update: Partial + ) => void; + getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap; } -export const DataVisualizerDataGrid = ({ +export const DataVisualizerTable = ({ items, pageState, updatePageState, -}: DataVisualizerDataGrid) => { + getItemIdToExpandedRowMap, +}: DataVisualizerTableProps) => { const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [expandAll, toggleExpandAll] = useState(false); - const { onTableChange, pagination, sorting } = useTableSettings( + const { onTableChange, pagination, sorting } = useTableSettings( items, pageState, updatePageState ); - const showDistributions: boolean = pageState.showDistributions ?? true; + const showDistributions: boolean = + ('showDistributions' in pageState && pageState.showDistributions) ?? true; const toggleShowDistribution = () => { updatePageState({ ...pageState, @@ -73,7 +76,7 @@ export const DataVisualizerDataGrid = ({ }); }; - function toggleDetails(item: FieldVisConfig) { + function toggleDetails(item: DataVisualizerTableItem) { if (item.fieldName === undefined) return; const index = expandedRowItemIds.indexOf(item.fieldName); if (index !== -1) { @@ -87,7 +90,7 @@ export const DataVisualizerDataGrid = ({ } const columns = useMemo(() => { - const expanderColumn: EuiTableComputedColumnType = { + const expanderColumn: EuiTableComputedColumnType = { name: ( { + render: (item: DataVisualizerTableItem) => { if (item.fieldName === undefined) return null; const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown'; return ( @@ -167,8 +170,10 @@ export const DataVisualizerDataGrid = ({ name: i18n.translate('xpack.ml.datavisualizer.dataGrid.documentsCountColumnName', { defaultMessage: 'Documents (%)', }), - render: (value: number | undefined, item: FieldVisConfig) => , - sortable: (item: FieldVisConfig) => item?.stats?.count, + render: (value: number | undefined, item: DataVisualizerTableItem) => ( + + ), + sortable: (item: DataVisualizerTableItem) => item?.stats?.count, align: LEFT_ALIGNMENT as HorizontalAlignment, 'data-test-subj': 'mlDataVisualizerTableColumnDocumentsCount', }, @@ -203,15 +208,27 @@ export const DataVisualizerDataGrid = ({ />
), - render: (item: FieldVisConfig) => { + render: (item: DataVisualizerTableItem) => { if (item === undefined || showDistributions === false) return null; - if (item.type === 'keyword' && item.stats?.topValues !== undefined) { + if ( + (item.type === ML_JOB_FIELD_TYPES.KEYWORD || item.type === ML_JOB_FIELD_TYPES.IP) && + item.stats?.topValues !== undefined + ) { return ; } - if (item.type === 'number' && item.stats?.distribution !== undefined) { - return ; + if (item.type === ML_JOB_FIELD_TYPES.NUMBER) { + if (isIndexBasedFieldVisConfig(item) && item.stats?.distribution !== undefined) { + return ; + } else { + return ; + } } + + if (item.type === ML_JOB_FIELD_TYPES.BOOLEAN) { + return ; + } + return null; }, align: LEFT_ALIGNMENT as HorizontalAlignment, @@ -230,7 +247,7 @@ export const DataVisualizerDataGrid = ({ return ( - + className={'mlDataVisualizer'} items={items} itemId={FIELD_NAME} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/index.ts new file mode 100644 index 0000000000000..787bd71fce481 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { useDataVizChartTheme } from './use_data_viz_chart_theme'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/use_data_viz_chart_theme.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/use_data_viz_chart_theme.ts new file mode 100644 index 0000000000000..14e83da0546a6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/hooks/use_data_viz_chart_theme.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PartialTheme } from '@elastic/charts'; +import { useMemo } from 'react'; +import { useCurrentEuiTheme } from '../../../components/color_range_legend'; +export const useDataVizChartTheme = (): PartialTheme => { + const { euiTheme } = useCurrentEuiTheme(); + const chartTheme = useMemo(() => { + const AREA_SERIES_COLOR = euiTheme.euiColorVis0; + return { + axes: { + tickLabel: { + fontSize: parseInt(euiTheme.euiFontSizeXS, 10), + fontFamily: euiTheme.euiFontFamily, + fontStyle: 'italic', + }, + }, + background: { color: 'transparent' }, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + chartPaddings: { + left: 0, + right: 0, + top: 4, + bottom: 0, + }, + scales: { barsPadding: 0.1 }, + colors: { + vizColors: [AREA_SERIES_COLOR], + }, + areaSeriesStyle: { + line: { + strokeWidth: 1, + visible: true, + }, + point: { + visible: false, + radius: 0, + opacity: 0, + }, + area: { visible: true, opacity: 1 }, + }, + }; + }, [euiTheme]); + return chartTheme; +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/index.ts new file mode 100644 index 0000000000000..f903113a39ca6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataVisualizerTable, ItemIdToExpandedRowMap } from './data_visualizer_stats_table'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_data_row.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_data_row.ts new file mode 100644 index 0000000000000..4f52534d9d75b --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_data_row.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config'; + +export interface FieldDataRowProps { + config: FieldVisConfig | FileBasedFieldVisConfig; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts similarity index 69% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts rename to x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts index 4783107742799..a35765dd9b15e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts @@ -50,7 +50,7 @@ export interface FieldVisStats { max?: number; median?: number; min?: number; - topValues?: Array<{ key: number; doc_count: number }>; + topValues?: Array<{ key: number | string; doc_count: number }>; topValuesSampleSize?: number; topValuesSamplerShardSize?: number; examples?: Array; @@ -70,3 +70,28 @@ export interface FieldVisConfig { fieldFormat?: any; isUnsupportedType?: boolean; } + +export interface FileBasedFieldVisConfig { + type: MlJobFieldType; + fieldName?: string; + stats?: FieldVisStats; + format?: string; +} + +export interface FileBasedUnknownFieldVisConfig { + fieldName: string; + type: 'text' | 'unknown'; + stats: { mean: number; count: number; sampleCount: number; cardinality: number }; +} + +export function isFileBasedFieldVisConfig( + field: FieldVisConfig | FileBasedFieldVisConfig +): field is FileBasedFieldVisConfig { + return !field.hasOwnProperty('existsInDocs'); +} + +export function isIndexBasedFieldVisConfig( + field: FieldVisConfig | FileBasedFieldVisConfig +): field is FieldVisConfig { + return field.hasOwnProperty('existsInDocs'); +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/index.ts new file mode 100644 index 0000000000000..439d4f037ca15 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FieldDataRowProps } from './field_data_row'; +export { + FieldVisConfig, + FileBasedFieldVisConfig, + FieldVisStats, + MetricFieldVisStats, + isFileBasedFieldVisConfig, + isIndexBasedFieldVisConfig, +} from './field_vis_config'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/utils.ts new file mode 100644 index 0000000000000..ead30b9498a62 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/utils.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FileBasedFieldVisConfig } from './types'; + +export const getTFPercentage = (config: FileBasedFieldVisConfig) => { + const { stats } = config; + if (stats === undefined) return null; + const { count } = stats; + // use stats from index based config + let { trueCount, falseCount } = stats; + + // use stats from file based find structure results + if (stats.trueCount === undefined || stats.falseCount === undefined) { + if (config?.stats?.topValues) { + config.stats.topValues.forEach((doc) => { + if (doc.doc_count !== undefined) { + if (doc.key.toString().toLowerCase() === 'false') { + falseCount = doc.doc_count; + } + if (doc.key.toString().toLowerCase() === 'true') { + trueCount = doc.doc_count; + } + } + }); + } + } + if (count === undefined || trueCount === undefined || falseCount === undefined) return null; + return { + count, + trueCount, + falseCount, + }; +}; diff --git a/x-pack/plugins/ml/public/application/formatters/round_to_decimal_place.ts b/x-pack/plugins/ml/public/application/formatters/round_to_decimal_place.ts index 5a030d7619e98..88a82da5ed9d0 100644 --- a/x-pack/plugins/ml/public/application/formatters/round_to_decimal_place.ts +++ b/x-pack/plugins/ml/public/application/formatters/round_to_decimal_place.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function roundToDecimalPlace(num: number, dp: number = 2): number | string { +export function roundToDecimalPlace(num?: number, dp: number = 2): number | string { + if (num === undefined) return ''; if (num % 1 === 0) { // no decimal place return num; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0c432a039b112..65298463c9808 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13126,18 +13126,6 @@ "xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "まとめ", "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "トップの値", "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "トップの値", - "xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布", - "xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "トップの値", - "xpack.ml.fieldDataCard.cardNumber.displayingPercentilesLabel": "{minPercent} - {maxPercent} パーセンタイルを表示中", - "xpack.ml.fieldDataCard.cardNumber.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, other {値}}", - "xpack.ml.fieldDataCard.cardNumber.documentsCountDescription": "{count, plural, other {# 個のドキュメント}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardNumber.maxLabel": "最高", - "xpack.ml.fieldDataCard.cardNumber.medianLabel": "中間", - "xpack.ml.fieldDataCard.cardNumber.minLabel": "分", - "xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel": "メトリック詳細の表示オプションを選択してください", - "xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} タイプ", - "xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, other {値}}", - "xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, other {# 個のドキュメント}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "たとえば、ドキュメントマッピングで {copyToParam} パラメーターを使ったり、{includesParam} と {excludesParam} パラメーターを使用してインデックスした後に {sourceParam} フィールドから切り取ったりして入力される場合があります。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "このフィールドはクエリが実行されたドキュメントの {sourceParam} フィールドにありませんでした。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "このフィールドの例が取得されませんでした", @@ -13221,13 +13209,9 @@ "xpack.ml.fileDatavisualizer.explanationFlyout.closeButton": "閉じる", "xpack.ml.fileDatavisualizer.explanationFlyout.content": "分析結果を生成した論理ステップ。", "xpack.ml.fileDatavisualizer.explanationFlyout.title": "分析説明", - "xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription": "{fieldCardinality} 個の特徴的な {fieldCardinality, plural, other {値}}", - "xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription": "{fieldCount, plural, other {# 個のドキュメント}} ({fieldPercent}%)", "xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle": "最高", "xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle": "中間", "xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle": "分", - "xpack.ml.fileDatavisualizer.fieldStatsCard.noFieldInformationAvailableDescription": "フィールド情報がありません", - "xpack.ml.fileDatavisualizer.fieldStatsCard.topStatsValuesDescription": "トップの値", "xpack.ml.fileDatavisualizer.fileBeatConfig.paths": "ファイルのパスをここに追加してください", "xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.closeButton": "閉じる", "xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.copyButton": "クリップボードにコピー", @@ -13314,7 +13298,6 @@ "xpack.ml.fileDatavisualizer.resultsLinks.openInDataVisualizerTitle": "データビジュアライザーを開く", "xpack.ml.fileDatavisualizer.resultsLinks.viewIndexInDiscoverTitle": "インデックスをディスカバリで表示", "xpack.ml.fileDatavisualizer.resultsView.analysisExplanationButtonLabel": "分析説明", - "xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName": "ファイル統計", "xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel": "上書き設定", "xpack.ml.fileDatavisualizer.simpleImportSettings.createIndexPatternLabel": "インデックスパターンを作成", "xpack.ml.fileDatavisualizer.simpleImportSettings.indexNameAriaLabel": "インデックス名、必須フィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 627e7e18bf617..7befbcf34e4d8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13157,18 +13157,6 @@ "xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "摘要", "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "排名最前值", "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "排名最前值", - "xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布", - "xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "排名最前值", - "xpack.ml.fieldDataCard.cardNumber.displayingPercentilesLabel": "显示 {minPercent} - {maxPercent} 百分位数", - "xpack.ml.fieldDataCard.cardNumber.distinctCountDescription": "{cardinality} 个不同的 {cardinality, plural, other {值}}", - "xpack.ml.fieldDataCard.cardNumber.documentsCountDescription": "{count, plural, other {# 个文档}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardNumber.maxLabel": "最大值", - "xpack.ml.fieldDataCard.cardNumber.medianLabel": "中值", - "xpack.ml.fieldDataCard.cardNumber.minLabel": "最小值", - "xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel": "选择指标详情的显示选项", - "xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} 类型", - "xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 个不同的 {cardinality, plural, other {值}}", - "xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "例如,可以使用文档映射中的 {copyToParam} 参数进行填充,也可以在索引后通过使用 {includesParam} 和 {excludesParam} 参数从 {sourceParam} 字段中修剪。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "查询的文档的 {sourceParam} 字段中不存在此字段。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "没有获取此字段的示例", @@ -13252,13 +13240,9 @@ "xpack.ml.fileDatavisualizer.explanationFlyout.closeButton": "关闭", "xpack.ml.fileDatavisualizer.explanationFlyout.content": "产生分析结果的逻辑步骤。", "xpack.ml.fileDatavisualizer.explanationFlyout.title": "分析说明", - "xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription": "{fieldCardinality} 个不同的{fieldCardinality, plural, other {值}}", - "xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription": "{fieldCount, plural, other {# 个文档}} ({fieldPercent}%)", "xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle": "最大值", "xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle": "中值", "xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle": "最小值", - "xpack.ml.fileDatavisualizer.fieldStatsCard.noFieldInformationAvailableDescription": "没有可用的字段信息", - "xpack.ml.fileDatavisualizer.fieldStatsCard.topStatsValuesDescription": "排在前面的值", "xpack.ml.fileDatavisualizer.fileBeatConfig.paths": "在此处将路径添加您的文件中", "xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.closeButton": "关闭", "xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.copyButton": "复制到剪贴板", @@ -13346,7 +13330,6 @@ "xpack.ml.fileDatavisualizer.resultsLinks.openInDataVisualizerTitle": "在数据可视化工具中打开", "xpack.ml.fileDatavisualizer.resultsLinks.viewIndexInDiscoverTitle": "在 Discover 中查看索引", "xpack.ml.fileDatavisualizer.resultsView.analysisExplanationButtonLabel": "分析说明", - "xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName": "文件统计", "xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel": "替代设置", "xpack.ml.fileDatavisualizer.simpleImportSettings.createIndexPatternLabel": "创建索引模式", "xpack.ml.fileDatavisualizer.simpleImportSettings.indexNameAriaLabel": "索引名称,必填字段", diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index fc0c339ca2693..531eba54f931d 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -7,6 +7,7 @@ import path from 'path'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); @@ -17,11 +18,98 @@ export default function ({ getService }: FtrProviderContext) { filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'), indexName: 'user-import_1', createIndexPattern: false, + fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER, ML_JOB_FIELD_TYPES.DATE], + fieldNameFilters: ['clientip'], expected: { results: { title: 'artificial_server_log', numberOfFields: 4, }, + metricFields: [ + { + fieldName: 'bytes', + type: ML_JOB_FIELD_TYPES.NUMBER, + docCountFormatted: '19 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 8, + }, + { + fieldName: 'httpversion', + type: ML_JOB_FIELD_TYPES.NUMBER, + docCountFormatted: '19 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 1, + }, + { + fieldName: 'response', + type: ML_JOB_FIELD_TYPES.NUMBER, + docCountFormatted: '19 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 3, + }, + ], + nonMetricFields: [ + { + fieldName: 'timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + docCountFormatted: '19 (100%)', + exampleCount: 10, + }, + { + fieldName: 'agent', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 8, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'auth', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 1, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'ident', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 1, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'verb', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 1, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'request', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 2, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'referrer', + type: ML_JOB_FIELD_TYPES.KEYWORD, + exampleCount: 1, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'clientip', + type: ML_JOB_FIELD_TYPES.IP, + exampleCount: 7, + docCountFormatted: '19 (100%)', + }, + { + fieldName: 'message', + type: ML_JOB_FIELD_TYPES.TEXT, + exampleCount: 10, + docCountFormatted: '19 (100%)', + }, + ], + visibleMetricFieldsCount: 3, + totalMetricFieldsCount: 3, + populatedFieldsCount: 12, + totalFieldsCount: 12, + fieldTypeFiltersResultCount: 4, + fieldNameFiltersResultCount: 1, }, }, ]; @@ -63,8 +151,65 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataVisualizerFileBased.assertFileContentPanelExists(); await ml.dataVisualizerFileBased.assertSummaryPanelExists(); await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); - await ml.dataVisualizerFileBased.assertNumberOfFieldCards( - testData.expected.results.numberOfFields + + await ml.testExecution.logTestStep( + `displays elements in the data visualizer table correctly` + ); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); + + await ml.dataVisualizerIndexBased.assertVisibleMetricFieldsCount( + testData.expected.visibleMetricFieldsCount + ); + await ml.dataVisualizerIndexBased.assertTotalMetricFieldsCount( + testData.expected.totalMetricFieldsCount + ); + await ml.dataVisualizerIndexBased.assertVisibleFieldsCount( + testData.expected.totalFieldsCount + ); + await ml.dataVisualizerIndexBased.assertTotalFieldsCount( + testData.expected.totalFieldsCount + ); + + await ml.testExecution.logTestStep( + 'displays details for metric fields and non-metric fields correctly' + ); + await ml.dataVisualizerTable.ensureNumRowsPerPage(25); + + for (const fieldRow of testData.expected.metricFields) { + await ml.dataVisualizerTable.assertNumberFieldContents( + fieldRow.fieldName, + fieldRow.docCountFormatted, + fieldRow.topValuesCount, + false + ); + } + for (const fieldRow of testData.expected.nonMetricFields!) { + await ml.dataVisualizerTable.assertNonMetricFieldContents( + fieldRow.type, + fieldRow.fieldName!, + fieldRow.docCountFormatted, + fieldRow.exampleCount + ); + } + + await ml.testExecution.logTestStep('sets and resets field type filter correctly'); + await ml.dataVisualizerTable.setFieldTypeFilter( + testData.fieldTypeFilters, + testData.expected.fieldTypeFiltersResultCount + ); + await ml.dataVisualizerTable.removeFieldTypeFilter( + testData.fieldTypeFilters, + testData.expected.totalFieldsCount + ); + + await ml.testExecution.logTestStep('sets and resets field name filter correctly'); + await ml.dataVisualizerTable.setFieldNameFilter( + testData.fieldNameFilters, + testData.expected.fieldNameFiltersResultCount + ); + await ml.dataVisualizerTable.removeFieldNameFilter( + testData.fieldNameFilters, + testData.expected.totalFieldsCount ); await ml.testExecution.logTestStep('loads the import settings page'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/artificial_server_log b/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/artificial_server_log index 3571d3c9b5e42..d9be69996b86a 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/artificial_server_log +++ b/x-pack/test/functional/apps/ml/data_visualizer/files_to_import/artificial_server_log @@ -1,19 +1,20 @@ -2018-01-06 16:56:14.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.0 -2018-01-06 16:56:15.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.1 -2018-01-06 16:56:16.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.2 -2018-01-06 16:56:17.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.3 -2018-01-06 16:56:18.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.0 -2018-01-06 16:56:19.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.2 -2018-01-06 16:56:20.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.3 -2018-01-06 16:56:21.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.4 -2018-01-06 16:56:22.295748 WARN host:'Server A' Disk watermark 80% -2018-01-06 17:16:23.295748 WARN host:'Server A' Disk watermark 90% -2018-01-06 17:36:10.295748 ERROR host:'Server A' Main process crashed -2018-01-06 17:36:14.295748 INFO host:'Server A' Connection from ip 123.456.789.0 closed -2018-01-06 17:36:15.295748 INFO host:'Server A' Connection from ip 123.456.789.1 closed -2018-01-06 17:36:16.295748 INFO host:'Server A' Connection from ip 123.456.789.2 closed -2018-01-06 17:36:17.295748 INFO host:'Server A' Connection from ip 123.456.789.3 closed -2018-01-06 17:46:11.295748 INFO host:'Server B' Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß -2018-01-06 17:46:12.295748 INFO host:'Server B' Shutting down - - +93.180.71.3 - - [17/May/2015:08:05:32 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" +93.180.71.3 - - [17/May/2015:08:05:23 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" +80.91.33.133 - - [17/May/2015:08:05:24 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)" +217.168.17.5 - - [17/May/2015:08:05:34 +0000] "GET /downloads/product_1 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)" +217.168.17.5 - - [17/May/2015:08:05:09 +0000] "GET /downloads/product_2 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)" +93.180.71.3 - - [17/May/2015:08:05:57 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" +217.168.17.5 - - [17/May/2015:08:05:02 +0000] "GET /downloads/product_2 HTTP/1.1" 404 337 "-" "Debian APT-HTTP/1.3 (0.8.10.3)" +217.168.17.5 - - [17/May/2015:08:05:42 +0000] "GET /downloads/product_1 HTTP/1.1" 404 332 "-" "Debian APT-HTTP/1.3 (0.8.10.3)" +80.91.33.133 - - [17/May/2015:08:05:01 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)" +93.180.71.3 - - [17/May/2015:08:05:27 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" +217.168.17.5 - - [17/May/2015:08:05:12 +0000] "GET /downloads/product_2 HTTP/1.1" 200 3316 "-" "Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß" +188.138.60.101 - - [17/May/2015:08:05:49 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)" +80.91.33.133 - - [17/May/2015:08:05:14 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.16)" +46.4.66.76 - - [17/May/2015:08:05:45 +0000] "GET /downloads/product_1 HTTP/1.1" 404 318 "-" "Debian APT-HTTP/1.3 (1.0.1ubuntu2)" +93.180.71.3 - - [17/May/2015:08:05:26 +0000] "GET /downloads/product_1 HTTP/1.1" 404 324 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" +91.234.194.89 - - [17/May/2015:08:05:22 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)" +80.91.33.133 - - [17/May/2015:08:05:07 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)" +37.26.93.214 - - [17/May/2015:08:05:38 +0000] "GET /downloads/product_2 HTTP/1.1" 404 319 "-" "Go 1.1 package http" +188.138.60.101 - - [17/May/2015:08:05:25 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)" +93.180.71.3 - - [17/May/2015:08:05:11 +0000] "GET /downloads/product_1 HTTP/1.1" 404 340 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)" diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 5a8b9bfc114ee..0833f84960ea6 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -6,7 +6,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; -import { FieldVisConfig } from '../../../../../plugins/ml/public/application/datavisualizer/index_based/common'; +import { FieldVisConfig } from '../../../../../plugins/ml/public/application/datavisualizer/stats_table/types'; interface MetricFieldVisConfig extends FieldVisConfig { statsMaxDecimalPlaces: number; diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index f8623842a596d..ad4625ed4dcb4 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -246,7 +246,8 @@ export function MachineLearningDataVisualizerTableProvider( public async assertNumberFieldContents( fieldName: string, docCountFormatted: string, - topValuesCount: number + topValuesCount: number, + checkDistributionPreviewExist = true ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); @@ -257,7 +258,9 @@ export function MachineLearningDataVisualizerTableProvider( await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlTopValues')); await this.assertTopValuesContents(fieldName, topValuesCount); - await this.assertDistributionPreviewExist(fieldName); + if (checkDistributionPreviewExist) { + await this.assertDistributionPreviewExist(fieldName); + } await this.ensureDetailsClosed(fieldName); } @@ -320,5 +323,19 @@ export function MachineLearningDataVisualizerTableProvider( await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); } } + + public async ensureNumRowsPerPage(n: 10 | 25 | 100) { + const paginationButton = 'mlDataVisualizerTable > tablePaginationPopoverButton'; + await retry.tryForTime(10000, async () => { + await testSubjects.existOrFail(paginationButton); + await testSubjects.click(paginationButton); + await testSubjects.click(`tablePagination-${n}-rows`); + + const visibleTexts = await testSubjects.getVisibleText(paginationButton); + + const [, pagination] = visibleTexts.split(': '); + expect(pagination).to.eql(n.toString()); + }); + } })(); } From fd9697c81321e78445ab9376e8eaf108701c994d Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Wed, 20 Jan 2021 12:20:24 -0500 Subject: [PATCH 08/28] Rename test spec file (#88842) --- .../timelines/{local_storage.sepc.ts => local_storage.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename x-pack/plugins/security_solution/cypress/integration/timelines/{local_storage.sepc.ts => local_storage.spec.ts} (100%) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.sepc.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.sepc.ts rename to x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts From a0af6bdea629fb77ca65605544a6473c1da3a079 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 20 Jan 2021 12:21:11 -0500 Subject: [PATCH 09/28] [CI] [TeamCity] Add more default ci groups and build usage_collection plugin (#88864) --- .ci/teamcity/default/build_plugins.sh | 1 + .teamcity/src/builds/default/DefaultCiGroups.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/teamcity/default/build_plugins.sh b/.ci/teamcity/default/build_plugins.sh index 76c553b4f8fa2..4b87596392239 100755 --- a/.ci/teamcity/default/build_plugins.sh +++ b/.ci/teamcity/default/build_plugins.sh @@ -14,6 +14,7 @@ node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --scan-dir "$XPACK_DIR/test/usage_collection/plugins" \ --verbose tc_end_block "Build Platform Plugins" diff --git a/.teamcity/src/builds/default/DefaultCiGroups.kt b/.teamcity/src/builds/default/DefaultCiGroups.kt index 4f39283149e73..948e2ab5782f9 100644 --- a/.teamcity/src/builds/default/DefaultCiGroups.kt +++ b/.teamcity/src/builds/default/DefaultCiGroups.kt @@ -3,7 +3,7 @@ package builds.default import dependsOn import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType -const val DEFAULT_CI_GROUP_COUNT = 11 +const val DEFAULT_CI_GROUP_COUNT = 13 val defaultCiGroups = (1..DEFAULT_CI_GROUP_COUNT).map { DefaultCiGroup(it) } object DefaultCiGroups : BuildType({ From 4878554cc96be9073cd47283ccd757dec333d8c3 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 20 Jan 2021 17:22:16 +0000 Subject: [PATCH 10/28] [Task Manager] cancel expired tasks as part of the available workers check (#88483) When a task expires it continues to reside in the queue until `TaskPool.cancelExpiredTasks()` is called. We call this in `TaskPool.run()`, but `run` won't get called if there is no capacity, as we gate the poller on `TaskPool.availableWorkers()` and that means that if you have as many expired tasks as you have workers - your poller will continually restart but the queue will remain full and that Task Manager is then in capable of taking on any more work. This is what caused `[Task Poller Monitor]: Observable Monitor: Hung Observable...` --- .../task_manager/server/task_pool.test.ts | 57 ++++++++++++++++++- .../plugins/task_manager/server/task_pool.ts | 27 +++++---- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 95768bb2f1afa..9161bbf3c28a5 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -210,7 +210,8 @@ describe('TaskPool', () => { logger, }); - const expired = resolvable(); + const readyToExpire = resolvable(); + const taskHasExpired = resolvable(); const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); const now = new Date(); @@ -218,8 +219,9 @@ describe('TaskPool', () => { { ...mockTask(), async run() { + await readyToExpire; this.isExpired = true; - expired.resolve(); + taskHasExpired.resolve(); await sleep(10); return asOk({ state: {} }); }, @@ -246,9 +248,11 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); - await expired; + readyToExpire.resolve(); + await taskHasExpired; expect(await pool.run([{ ...mockTask() }])).toBeTruthy(); + sinon.assert.calledOnce(shouldRun); sinon.assert.notCalled(shouldNotRun); @@ -260,6 +264,53 @@ describe('TaskPool', () => { ); }); + test('calls to availableWorkers ensures we cancel expired tasks', async () => { + const pool = new TaskPool({ + maxWorkers$: of(1), + logger: loggingSystemMock.create().get(), + }); + + const taskIsRunning = resolvable(); + const taskHasExpired = resolvable(); + const cancel = sinon.spy(() => Promise.resolve()); + const now = new Date(); + expect( + await pool.run([ + { + ...mockTask(), + async run() { + await sleep(10); + this.isExpired = true; + taskIsRunning.resolve(); + await taskHasExpired; + return asOk({ state: {} }); + }, + get expiration() { + return new Date(now.getTime() + 10); + }, + get startedAt() { + return now; + }, + cancel, + }, + ]) + ).toEqual(TaskPoolRunResult.RunningAtCapacity); + + await taskIsRunning; + + sinon.assert.notCalled(cancel); + expect(pool.occupiedWorkers).toEqual(1); + // The call to `availableWorkers` will clear the expired task so it's 1 instead of 0 + expect(pool.availableWorkers).toEqual(1); + sinon.assert.calledOnce(cancel); + + expect(pool.occupiedWorkers).toEqual(0); + expect(pool.availableWorkers).toEqual(1); + // ensure cancel isn't called twice + sinon.assert.calledOnce(cancel); + taskHasExpired.resolve(); + }); + test('logs if cancellation errors', async () => { const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 561a222310f3e..db17e75639ed9 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -85,6 +85,10 @@ export class TaskPool { // this should happen less often than the actual changes to the worker queue // so is lighter than emitting the load every time we add/remove a task from the queue this.load$.next(asTaskManagerStatEvent('load', asOk(this.workerLoad))); + // cancel expired task whenever a call is made to check for capacity + // this ensures that we don't end up with a queue of hung tasks causing both + // the poller and the pool from hanging due to lack of capacity + this.cancelExpiredTasks(); return this.maxWorkers - this.occupiedWorkers; } @@ -96,19 +100,7 @@ export class TaskPool { * @param {TaskRunner[]} tasks * @returns {Promise} */ - public run = (tasks: TaskRunner[]) => { - this.cancelExpiredTasks(); - return this.attemptToRun(tasks); - }; - - public cancelRunningTasks() { - this.logger.debug('Cancelling running tasks.'); - for (const task of this.running) { - this.cancelTask(task); - } - } - - private async attemptToRun(tasks: TaskRunner[]): Promise { + public run = async (tasks: TaskRunner[]): Promise => { const [tasksToRun, leftOverTasks] = partitionListByCount(tasks, this.availableWorkers); if (tasksToRun.length) { performance.mark('attemptToRun_start'); @@ -135,13 +127,20 @@ export class TaskPool { if (leftOverTasks.length) { if (this.availableWorkers) { - return this.attemptToRun(leftOverTasks); + return this.run(leftOverTasks); } return TaskPoolRunResult.RanOutOfCapacity; } else if (!this.availableWorkers) { return TaskPoolRunResult.RunningAtCapacity; } return TaskPoolRunResult.RunningAllClaimedTasks; + }; + + public cancelRunningTasks() { + this.logger.debug('Cancelling running tasks.'); + for (const task of this.running) { + this.cancelTask(task); + } } private handleMarkAsRunning(taskRunner: TaskRunner) { From e21defa448d9bdf3de46201ab84cc88ef1c4e90e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 20 Jan 2021 17:23:02 +0000 Subject: [PATCH 11/28] [Task Manager] Reject invalid Timeout values in Task Type Definitions (#88602) This PR adds the following: 1. We now validate the interval passed to `timeout` when a task type definition is registered. 2. replaces usage of `Joi` with `schema-type` --- .../task_manager/server/lib/intervals.ts | 35 ++++-- .../task_manager/server/lib/result_type.ts | 8 ++ x-pack/plugins/task_manager/server/task.ts | 101 +++++++++--------- .../server/task_running/task_runner.test.ts | 54 +--------- .../server/task_running/task_runner.ts | 19 +--- .../server/task_type_dictionary.test.ts | 63 ++++++++++- .../server/task_type_dictionary.ts | 32 ++---- 7 files changed, 157 insertions(+), 155 deletions(-) diff --git a/x-pack/plugins/task_manager/server/lib/intervals.ts b/x-pack/plugins/task_manager/server/lib/intervals.ts index da04dffa4b5d1..b7945ff25d089 100644 --- a/x-pack/plugins/task_manager/server/lib/intervals.ts +++ b/x-pack/plugins/task_manager/server/lib/intervals.ts @@ -12,6 +12,19 @@ export enum IntervalCadence { Hour = 'h', Day = 'd', } + +// Once Babel is updated ot support Typescript 4.x templated types, we can use +// this more accurate and safer compile-time valdiation +// export type Interval = `${number}${IntervalCadence}`; +export type Interval = string; + +export function isInterval(interval: Interval | string): interval is Interval { + const numericAsStr: string = interval.slice(0, -1); + const numeric: number = parseInt(numericAsStr, 10); + const cadence: IntervalCadence | string = interval.slice(-1); + return !(!isCadence(cadence) || isNaN(numeric) || numeric <= 0 || !isNumeric(numericAsStr)); +} + const VALID_CADENCE = new Set(Object.values(IntervalCadence)); const CADENCE_IN_MS: Record = { [IntervalCadence.Second]: 1000, @@ -24,7 +37,7 @@ function isCadence(cadence: IntervalCadence | string): cadence is IntervalCadenc return VALID_CADENCE.has(cadence as IntervalCadence); } -export function asInterval(ms: number): string { +export function asInterval(ms: number): Interval { const secondsRemainder = ms % 1000; const minutesRemainder = ms % 60000; return secondsRemainder ? `${ms}ms` : minutesRemainder ? `${ms / 1000}s` : `${ms / 60000}m`; @@ -34,9 +47,9 @@ export function asInterval(ms: number): string { * Returns a date that is the specified interval from now. Currently, * only minute-intervals and second-intervals are supported. * - * @param {string} interval - An interval of the form `Nm` such as `5m` + * @param {Interval} interval - An interval of the form `Nm` such as `5m` */ -export function intervalFromNow(interval?: string): Date | undefined { +export function intervalFromNow(interval?: Interval): Date | undefined { if (interval === undefined) { return; } @@ -48,9 +61,9 @@ export function intervalFromNow(interval?: string): Date | undefined { * only minute-intervals and second-intervals are supported. * * @param {Date} date - The date to add interval to - * @param {string} interval - An interval of the form `Nm` such as `5m` + * @param {Interval} interval - An interval of the form `Nm` such as `5m` */ -export function intervalFromDate(date: Date, interval?: string): Date | undefined { +export function intervalFromDate(date: Date, interval?: Interval): Date | undefined { if (interval === undefined) { return; } @@ -59,9 +72,11 @@ export function intervalFromDate(date: Date, interval?: string): Date | undefine export function maxIntervalFromDate( date: Date, - ...intervals: Array + ...intervals: Array ): Date | undefined { - const maxSeconds = Math.max(...intervals.filter(isString).map(parseIntervalAsSecond)); + const maxSeconds = Math.max( + ...intervals.filter(isString).map((interval) => parseIntervalAsSecond(interval as Interval)) + ); if (!isNaN(maxSeconds)) { return secondsFromDate(date, maxSeconds); } @@ -91,14 +106,14 @@ export function secondsFromDate(date: Date, secs: number): Date { /** * Verifies that the specified interval matches our expected format. * - * @param {string} interval - An interval such as `5m` or `10s` + * @param {Interval} interval - An interval such as `5m` or `10s` * @returns {number} The interval as seconds */ -export const parseIntervalAsSecond = memoize((interval: string): number => { +export const parseIntervalAsSecond = memoize((interval: Interval): number => { return Math.round(parseIntervalAsMillisecond(interval) / 1000); }); -export const parseIntervalAsMillisecond = memoize((interval: string): number => { +export const parseIntervalAsMillisecond = memoize((interval: Interval): number => { const numericAsStr: string = interval.slice(0, -1); const numeric: number = parseInt(numericAsStr, 10); const cadence: IntervalCadence | string = interval.slice(-1); diff --git a/x-pack/plugins/task_manager/server/lib/result_type.ts b/x-pack/plugins/task_manager/server/lib/result_type.ts index d21c17d3bb5b3..cd1d417c79490 100644 --- a/x-pack/plugins/task_manager/server/lib/result_type.ts +++ b/x-pack/plugins/task_manager/server/lib/result_type.ts @@ -39,6 +39,14 @@ export function isErr(result: Result): result is Err { return !isOk(result); } +export function tryAsResult(fn: () => T): Result { + try { + return asOk(fn()); + } catch (e) { + return asErr(e); + } +} + export async function promiseResult(future: Promise): Promise> { try { return asOk(await future); diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index e832a95ac3caa..9e2a2a2074a84 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { Interval, isInterval, parseIntervalAsMillisecond } from './lib/intervals'; +import { isErr, tryAsResult } from './lib/result_type'; /* * Type definitions and validations for tasks. @@ -83,17 +85,8 @@ export interface FailedTaskResult { status: TaskStatus.Failed; } -export const validateRunResult = Joi.object({ - runAt: Joi.date().optional(), - schedule: Joi.object().optional(), - error: Joi.object().optional(), - state: Joi.object().optional(), -}).optional(); - export type RunFunction = () => Promise; - export type CancelFunction = () => Promise; - export interface CancellableTask { run: RunFunction; cancel?: CancelFunction; @@ -101,40 +94,53 @@ export interface CancellableTask { export type TaskRunCreatorFunction = (context: RunContext) => CancellableTask; +export const taskDefinitionSchema = schema.object( + { + /** + * A unique identifier for the type of task being defined. + */ + type: schema.string(), + /** + * A brief, human-friendly title for this task. + */ + title: schema.maybe(schema.string()), + /** + * An optional more detailed description of what this task does. + */ + description: schema.maybe(schema.string()), + /** + * How long, in minutes or seconds, the system should wait for the task to complete + * before it is considered to be timed out. (e.g. '5m', the default). If + * the task takes longer than this, Kibana will send it a kill command and + * the task will be re-attempted. + */ + timeout: schema.string({ + defaultValue: '5m', + }), + /** + * Up to how many times the task should retry when it fails to run. This will + * default to the global variable. + */ + maxAttempts: schema.maybe( + schema.number({ + min: 1, + }) + ), + }, + { + validate({ timeout }) { + if (!isInterval(timeout) || isErr(tryAsResult(() => parseIntervalAsMillisecond(timeout)))) { + return `Invalid timeout "${timeout}". Timeout must be of the form "{number}{cadance}" where number is an integer. Example: 5m.`; + } + }, + } +); + /** * Defines a task which can be scheduled and run by the Kibana * task manager. */ -export interface TaskDefinition { - /** - * A unique identifier for the type of task being defined. - */ - type: string; - - /** - * A brief, human-friendly title for this task. - */ - title: string; - - /** - * An optional more detailed description of what this task does. - */ - description?: string; - - /** - * How long, in minutes or seconds, the system should wait for the task to complete - * before it is considered to be timed out. (e.g. '5m', the default). If - * the task takes longer than this, Kibana will send it a kill command and - * the task will be re-attempted. - */ - timeout?: string; - - /** - * Up to how many times the task should retry when it fails to run. This will - * default to the global variable. - */ - maxAttempts?: number; - +export type TaskDefinition = TypeOf & { /** * Function that customizes how the task should behave when the task fails. This * function can return `true`, `false` or a Date. True will tell task manager @@ -149,17 +155,7 @@ export interface TaskDefinition { * and an optional cancel function which cancels the task. */ createTaskRunner: TaskRunCreatorFunction; -} - -export const validateTaskDefinition = Joi.object({ - type: Joi.string().required(), - title: Joi.string().optional(), - description: Joi.string().optional(), - timeout: Joi.string().default('5m'), - maxAttempts: Joi.number().min(1).optional(), - createTaskRunner: Joi.func().required(), - getRetry: Joi.func().optional(), -}).default(); +}; export enum TaskStatus { Idle = 'idle', @@ -174,12 +170,11 @@ export enum TaskLifecycleResult { } export type TaskLifecycle = TaskStatus | TaskLifecycleResult; - export interface IntervalSchedule { /** * An interval in minutes (e.g. '5m'). If specified, this is a recurring task. * */ - interval: string; + interval: Interval; } /* diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index 3777d89ce63dd..77434d2b6559c 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -10,10 +10,10 @@ import { secondsFromNow } from '../lib/intervals'; import { asOk, asErr } from '../lib/result_type'; import { TaskManagerRunner, TaskRunResult } from '../task_running'; import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent, TaskRun } from '../task_events'; -import { ConcreteTaskInstance, TaskStatus, TaskDefinition, SuccessfulRunResult } from '../task'; +import { ConcreteTaskInstance, TaskStatus } from '../task'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import moment from 'moment'; -import { TaskTypeDictionary } from '../task_type_dictionary'; +import { TaskDefinitionRegistry, TaskTypeDictionary } from '../task_type_dictionary'; import { mockLogger } from '../test_utils'; import { throwUnrecoverableError } from './errors'; @@ -41,24 +41,6 @@ describe('TaskManagerRunner', () => { expect(runner.toString()).toEqual('bar "foo"'); }); - test('warns if the task returns an unexpected result', async () => { - await allowsReturnType(undefined); - await allowsReturnType({}); - await allowsReturnType({ - runAt: new Date(), - }); - await allowsReturnType({ - error: new Error('Dang it!'), - }); - await allowsReturnType({ - state: { shazm: true }, - }); - await disallowsReturnType('hm....'); - await disallowsReturnType({ - whatIsThis: '?!!?', - }); - }); - test('queues a reattempt if the task fails', async () => { const initialAttempts = _.random(0, 2); const id = Date.now().toString(); @@ -1121,7 +1103,7 @@ describe('TaskManagerRunner', () => { interface TestOpts { instance?: Partial; - definitions?: Record>; + definitions?: TaskDefinitionRegistry; onTaskEvent?: (event: TaskEvent) => void; } @@ -1196,34 +1178,4 @@ describe('TaskManagerRunner', () => { instance, }; } - - async function testReturn(result: unknown, shouldBeValid: boolean) { - const { runner, logger } = testOpts({ - definitions: { - bar: { - title: 'Bar!', - createTaskRunner: () => ({ - run: async () => result as SuccessfulRunResult, - }), - }, - }, - }); - - await runner.run(); - - if (shouldBeValid) { - expect(logger.warn).not.toHaveBeenCalled(); - } else { - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn.mock.calls[0][0]).toMatch(/invalid task result/i); - } - } - - function allowsReturnType(result: unknown) { - return testReturn(result, true); - } - - function disallowsReturnType(result: unknown) { - return testReturn(result, false); - } }); diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index d281a65da332c..704386d88ea3a 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -13,7 +13,6 @@ import { Logger } from 'src/core/server'; import apm from 'elastic-apm-node'; import { performance } from 'perf_hooks'; -import Joi from 'joi'; import { identity, defaults, flow } from 'lodash'; import { Middleware } from '../lib/middleware'; @@ -36,7 +35,6 @@ import { FailedRunResult, FailedTaskResult, TaskDefinition, - validateRunResult, TaskStatus, } from '../task'; import { TaskTypeDictionary } from '../task_type_dictionary'; @@ -311,20 +309,9 @@ export class TaskManagerRunner implements TaskRunner { private validateResult( result?: SuccessfulRunResult | FailedRunResult | void ): Result { - const { error } = Joi.validate(result, validateRunResult); - - if (error) { - this.logger.warn(`Invalid task result for ${this}: ${error.message}`); - return asErr({ - error: new Error(`Invalid task result for ${this}: ${error.message}`), - state: {}, - }); - } - if (!result) { - return asOk(EMPTY_RUN_RESULT); - } - - return isFailedRunResult(result) ? asErr({ ...result, error: result.error }) : asOk(result); + return isFailedRunResult(result) + ? asErr({ ...result, error: result.error }) + : asOk(result || EMPTY_RUN_RESULT); } private shouldTryToScheduleRetry(): boolean { diff --git a/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts index e1d6ef17f5f9d..bd532c38725dd 100644 --- a/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts +++ b/x-pack/plugins/task_manager/server/task_type_dictionary.test.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { RunContext, TaskDefinition } from './task'; -import { sanitizeTaskDefinitions } from './task_type_dictionary'; +import { sanitizeTaskDefinitions, TaskDefinitionRegistry } from './task_type_dictionary'; interface Opts { numTasks: number; @@ -73,8 +73,9 @@ describe('taskTypeDictionary', () => { it('throws a validation exception for invalid task definition', () => { const runsanitize = () => { - const taskDefinitions = { + const taskDefinitions: TaskDefinitionRegistry = { some_kind_of_task: { + // @ts-ignore fail: 'extremely', // cause a validation failure type: 'breaky_task', title: 'Test XYZ', @@ -94,6 +95,62 @@ describe('taskTypeDictionary', () => { return sanitizeTaskDefinitions(taskDefinitions); }; - expect(runsanitize).toThrowError(); + expect(runsanitize).toThrowErrorMatchingInlineSnapshot( + `"[fail]: definition for this key is missing"` + ); + }); + + it('throws a validation exception for invalid timeout on task definition', () => { + const runsanitize = () => { + const taskDefinitions: TaskDefinitionRegistry = { + some_kind_of_task: { + title: 'Test XYZ', + timeout: '15 days', + description: `Actually this won't work`, + createTaskRunner() { + return { + async run() { + return { + state: {}, + }; + }, + }; + }, + }, + }; + + return sanitizeTaskDefinitions(taskDefinitions); + }; + + expect(runsanitize).toThrowErrorMatchingInlineSnapshot( + `"Invalid timeout \\"15 days\\". Timeout must be of the form \\"{number}{cadance}\\" where number is an integer. Example: 5m."` + ); + }); + + it('throws a validation exception for invalid floating point timeout on task definition', () => { + const runsanitize = () => { + const taskDefinitions: TaskDefinitionRegistry = { + some_kind_of_task: { + title: 'Test XYZ', + timeout: '1.5h', + description: `Actually this won't work`, + createTaskRunner() { + return { + async run() { + return { + state: {}, + }; + }, + }; + }, + }, + }; + + return sanitizeTaskDefinitions(taskDefinitions); + }; + + expect(runsanitize).toThrowErrorMatchingInlineSnapshot( + `"Invalid timeout \\"1.5h\\". Timeout must be of the form \\"{number}{cadance}\\" where number is an integer. Example: 5m."` + ); }); }); diff --git a/x-pack/plugins/task_manager/server/task_type_dictionary.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.ts index 451b5dd7cad52..c66b117bde882 100644 --- a/x-pack/plugins/task_manager/server/task_type_dictionary.ts +++ b/x-pack/plugins/task_manager/server/task_type_dictionary.ts @@ -3,23 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { TaskDefinition, validateTaskDefinition } from './task'; +import { TaskDefinition, taskDefinitionSchema } from './task'; import { Logger } from '../../../../src/core/server'; -/* - * The TaskManager is the public interface into the task manager system. This glues together - * all of the disparate modules in one integration point. The task manager operates in two different ways: - * - * - pre-init, it allows middleware registration, but disallows task manipulation - * - post-init, it disallows middleware registration, but allows task manipulation - * - * Due to its complexity, this is mostly tested by integration tests (see readme). - */ - -/** - * The public interface into the task manager system. - */ +export type TaskDefinitionRegistry = Record< + string, + Omit & Pick, 'timeout'> +>; export class TaskTypeDictionary { private definitions = new Map(); private logger: Logger; @@ -57,7 +47,7 @@ export class TaskTypeDictionary { * Method for allowing consumers to register task definitions into the system. * @param taskDefinitions - The Kibana task definitions dictionary */ - public registerTaskDefinitions(taskDefinitions: Record>) { + public registerTaskDefinitions(taskDefinitions: TaskDefinitionRegistry) { const duplicate = Object.keys(taskDefinitions).find((type) => this.definitions.has(type)); if (duplicate) { throw new Error(`Task ${duplicate} is already defined!`); @@ -79,10 +69,8 @@ export class TaskTypeDictionary { * * @param taskDefinitions - The Kibana task definitions dictionary */ -export function sanitizeTaskDefinitions( - taskDefinitions: Record> -): TaskDefinition[] { - return Object.entries(taskDefinitions).map(([type, rawDefinition]) => - Joi.attempt({ type, ...rawDefinition }, validateTaskDefinition) - ); +export function sanitizeTaskDefinitions(taskDefinitions: TaskDefinitionRegistry): TaskDefinition[] { + return Object.entries(taskDefinitions).map(([type, rawDefinition]) => { + return taskDefinitionSchema.validate({ type, ...rawDefinition }) as TaskDefinition; + }); } From 2e878f59f7dae4422dbf8db0a43a2bf3160be0c3 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Wed, 20 Jan 2021 11:29:07 -0600 Subject: [PATCH 12/28] Sync search query with url in advanced settings (#81829) --- src/plugins/advanced_settings/kibana.json | 2 +- .../management_app/advanced_settings.test.tsx | 62 ++++++++-- .../management_app/advanced_settings.tsx | 111 +++++++++++------- .../components/search/search.tsx | 13 +- .../management_app/lib/get_aria_name.test.ts | 9 ++ .../management_app/lib/get_aria_name.ts | 39 +++++- .../mount_management_section.tsx | 28 ++++- .../common/url/encode_uri_query.test.ts | 77 +++++++++++- .../common/url/encode_uri_query.ts | 33 +++++- src/plugins/kibana_utils/common/url/index.ts | 3 +- .../telemetry_management_section.tsx | 40 ++++--- 11 files changed, 324 insertions(+), 93 deletions(-) diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index df0d31a904c59..68b133f382c35 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -5,5 +5,5 @@ "ui": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "usageCollection"], - "requiredBundles": ["kibanaReact", "home"] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index f6490e2560c5f..aaa7b1c10a412 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Observable } from 'rxjs'; import { ReactWrapper } from 'enzyme'; -import { mountWithI18nProvider } from '@kbn/test/jest'; +import { mountWithI18nProvider, shallowWithI18nProvider } from '@kbn/test/jest'; import dedent from 'dedent'; import { PublicUiSettingsParams, @@ -17,9 +17,10 @@ import { UiSettingsType, } from '../../../../core/public'; import { FieldSetting } from './types'; -import { AdvancedSettingsComponent } from './advanced_settings'; +import { AdvancedSettings } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { ComponentRegistry } from '../component_registry'; +import { Search } from './components/search'; jest.mock('./components/field', () => ({ Field: () => { @@ -222,10 +223,30 @@ function mockConfig() { } describe('AdvancedSettings', () => { + const defaultQuery = 'test:string:setting'; + const mockHistory = { + listen: jest.fn(), + } as any; + const locationSpy = jest.spyOn(window, 'location', 'get'); + + afterAll(() => { + locationSpy.mockRestore(); + }); + + const mockQuery = (query = defaultQuery) => { + locationSpy.mockImplementation( + () => + ({ + search: `?query=${query}`, + } as any) + ); + }; + it('should render specific setting if given setting key', async () => { + mockQuery(); const component = mountWithI18nProvider( - { component .find('Field') .filterWhere( - (n: ReactWrapper) => - (n.prop('setting') as Record).name === 'test:string:setting' + (n: ReactWrapper) => (n.prop('setting') as Record).name === defaultQuery ) ).toHaveLength(1); }); it('should render read-only when saving is disabled', async () => { + mockQuery(); const component = mountWithI18nProvider( - { component .find('Field') .filterWhere( - (n: ReactWrapper) => - (n.prop('setting') as Record).name === 'test:string:setting' + (n: ReactWrapper) => (n.prop('setting') as Record).name === defaultQuery ) .prop('enableSaving') ).toBe(false); }); + + it('should render unfiltered with query parsing error', async () => { + const badQuery = 'category:(accessibility))'; + mockQuery(badQuery); + const { toasts } = notificationServiceMock.createStartContract(); + const getComponent = () => + shallowWithI18nProvider( + + ); + + expect(getComponent).not.toThrow(); + expect(toasts.addWarning).toHaveBeenCalledTimes(1); + const component = getComponent(); + expect(component.find(Search).prop('query').text).toEqual(''); + }); }); diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 1b38e9356cbb2..b9b447f739fad 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -8,22 +8,35 @@ import React, { Component } from 'react'; import { Subscription } from 'rxjs'; -import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; +import { UnregisterCallback } from 'history'; +import { parse } from 'query-string'; -import { useParams } from 'react-router-dom'; import { UiCounterMetricType } from '@kbn/analytics'; +import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; + +import { + IUiSettingsClient, + DocLinksStart, + ToastsStart, + ScopedHistory, +} from '../../../../core/public'; +import { url } from '../../../kibana_utils/public'; + import { CallOuts } from './components/call_outs'; import { Search } from './components/search'; import { Form } from './components/form'; import { AdvancedSettingsVoiceAnnouncement } from './components/advanced_settings_voice_announcement'; -import { IUiSettingsClient, DocLinksStart, ToastsStart } from '../../../../core/public/'; import { ComponentRegistry } from '../'; import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; import { FieldSetting, SettingsChanges } from './types'; +import { parseErrorMsg } from './components/search/search'; + +export const QUERY = 'query'; interface AdvancedSettingsProps { + history: ScopedHistory; enableSaving: boolean; uiSettings: IUiSettingsClient; dockLinks: DocLinksStart['links']; @@ -32,10 +45,6 @@ interface AdvancedSettingsProps { trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } -interface AdvancedSettingsComponentProps extends AdvancedSettingsProps { - queryText: string; -} - interface AdvancedSettingsState { footerQueryMatched: boolean; query: Query; @@ -44,30 +53,25 @@ interface AdvancedSettingsState { type GroupedSettings = Record; -export class AdvancedSettingsComponent extends Component< - AdvancedSettingsComponentProps, - AdvancedSettingsState -> { +export class AdvancedSettings extends Component { private settings: FieldSetting[]; private groupedSettings: GroupedSettings; private categoryCounts: Record; private categories: string[] = []; private uiSettingsSubscription?: Subscription; + private unregister: UnregisterCallback; - constructor(props: AdvancedSettingsComponentProps) { + constructor(props: AdvancedSettingsProps) { super(props); this.settings = this.initSettings(this.props.uiSettings); this.groupedSettings = this.initGroupedSettings(this.settings); this.categories = this.initCategories(this.groupedSettings); this.categoryCounts = this.initCategoryCounts(this.groupedSettings); - - const parsedQuery = Query.parse(this.props.queryText ? getAriaName(this.props.queryText) : ''); - this.state = { - query: parsedQuery, - footerQueryMatched: false, - filteredSettings: this.mapSettings(Query.execute(parsedQuery, this.settings)), - }; + this.state = this.getQueryState(undefined, true); + this.unregister = this.props.history.listen(({ search }) => { + this.setState(this.getQueryState(search)); + }); } init(config: IUiSettingsClient) { @@ -134,11 +138,50 @@ export class AdvancedSettingsComponent extends Component< } componentWillUnmount() { - if (this.uiSettingsSubscription) { - this.uiSettingsSubscription.unsubscribe(); + this.uiSettingsSubscription?.unsubscribe?.(); + this.unregister?.(); + } + + private getQuery(queryString: string, intialQuery = false): Query { + try { + const query = intialQuery ? getAriaName(queryString) : queryString ?? ''; + return Query.parse(query); + } catch ({ message }) { + this.props.toasts.addWarning({ + title: parseErrorMsg, + text: message, + }); + return Query.parse(''); } } + private getQueryText(search?: string): string { + const queryParams = parse(search ?? window.location.search) ?? {}; + return (queryParams[QUERY] as string) ?? ''; + } + + private getQueryState(search?: string, intialQuery = false): AdvancedSettingsState { + const queryString = this.getQueryText(search); + const query = this.getQuery(queryString, intialQuery); + const filteredSettings = this.mapSettings(Query.execute(query, this.settings)); + const footerQueryMatched = Object.keys(filteredSettings).length > 0; + + return { + query, + filteredSettings, + footerQueryMatched, + }; + } + + setUrlQuery(q: string = '') { + const search = url.addQueryParam(window.location.search, QUERY, q); + + this.props.history.push({ + pathname: '', // remove any route query param + search, + }); + } + mapConfig(config: IUiSettingsClient) { const all = config.getAll(); return Object.entries(all) @@ -167,18 +210,11 @@ export class AdvancedSettingsComponent extends Component< } onQueryChange = ({ query }: { query: Query }) => { - this.setState({ - query, - filteredSettings: this.mapSettings(Query.execute(query, this.settings)), - }); + this.setUrlQuery(query.text); }; clearQuery = () => { - this.setState({ - query: Query.parse(''), - footerQueryMatched: false, - filteredSettings: this.groupedSettings, - }); + this.setUrlQuery(''); }; onFooterQueryMatchChange = (matched: boolean) => { @@ -244,18 +280,3 @@ export class AdvancedSettingsComponent extends Component< ); } } - -export const AdvancedSettings = (props: AdvancedSettingsProps) => { - const { query } = useParams<{ query: string }>(); - return ( - - ); -}; diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx index cd22c3d6db723..bc875dd8de785 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx @@ -12,12 +12,19 @@ import { EuiSearchBar, EuiFormErrorText, Query } from '@elastic/eui'; import { getCategoryName } from '../../lib'; +export const CATEGORY_FIELD = 'category'; + interface SearchProps { categories: string[]; query: Query; onQueryChange: ({ query }: { query: Query }) => void; } +export const parseErrorMsg = i18n.translate( + 'advancedSettings.searchBar.unableToParseQueryErrorMessage', + { defaultMessage: 'Unable to parse query' } +); + export class Search extends PureComponent { private categories: Array<{ value: string; name: string }> = []; @@ -67,7 +74,7 @@ export class Search extends PureComponent { const filters = [ { type: 'field_value_selection' as const, - field: 'category', + field: CATEGORY_FIELD, name: i18n.translate('advancedSettings.categorySearchLabel', { defaultMessage: 'Category', }), @@ -78,10 +85,6 @@ export class Search extends PureComponent { let queryParseError; if (!this.state.isSearchTextValid) { - const parseErrorMsg = i18n.translate( - 'advancedSettings.searchBar.unableToParseQueryErrorMessage', - { defaultMessage: 'Unable to parse query' } - ); queryParseError = ( {`${parseErrorMsg}. ${this.state.parseErrorMessage}`} ); diff --git a/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts index abe0e37d606a4..c00ae028dfc4d 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts @@ -22,6 +22,15 @@ describe('Settings', function () { expect(getAriaName()).to.be(''); expect(getAriaName(undefined)).to.be(''); }); + + it('should preserve category string', function () { + expect(getAriaName('xPack:fooBar:foo_bar_baz category:(general)')).to.be( + 'x pack foo bar foo bar baz category:(general)' + ); + expect(getAriaName('xPack:fooBar:foo_bar_baz category:(general or discover)')).to.be( + 'x pack foo bar foo bar baz category:(general or discover)' + ); + }); }); }); }); diff --git a/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts index 1fe43fc6ba868..6d4817934083d 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts @@ -8,15 +8,46 @@ import { words } from 'lodash'; +import { Query } from '@elastic/eui'; + +import { CATEGORY_FIELD } from '../components/search/search'; + +const mapWords = (name?: string): string => + words(name ?? '') + .map((word) => word.toLowerCase()) + .join(' '); + /** * @name {string} the name of the configuration object * @returns {string} a space delimited, lowercase string with * special characters removed. * - * Example: 'xPack:fooBar:foo_bar_baz' -> 'x pack foo bar foo bar baz' + * Examples: + * - `xPack:fooBar:foo_bar_baz` -> `x pack foo bar foo bar baz` + * - `xPack:fooBar:foo_bar_baz category:(general)` -> `x pack foo bar foo bar baz category:(general)` */ export function getAriaName(name?: string) { - return words(name || '') - .map((word) => word.toLowerCase()) - .join(' '); + if (!name) { + return ''; + } + + const query = Query.parse(name); + + if (query.hasOrFieldClause(CATEGORY_FIELD)) { + const categories = query.getOrFieldClause(CATEGORY_FIELD); + const termValue = mapWords(query.removeOrFieldClauses(CATEGORY_FIELD).text); + + if (!categories || !Array.isArray(categories.value)) { + return termValue; + } + + let categoriesQuery = Query.parse(''); + categories.value.forEach((v) => { + categoriesQuery = categoriesQuery.addOrFieldValue(CATEGORY_FIELD, v); + }); + + return `${termValue} ${categoriesQuery.text}`; + } + + return mapWords(name); } diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index b48f3eff7453a..21a8a8cbd05ab 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -8,18 +8,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Router, Switch, Route } from 'react-router-dom'; +import { Router, Switch, Route, Redirect, RouteChildrenProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import { StartServicesAccessor } from 'src/core/public'; -import { AdvancedSettings } from './advanced_settings'; +import { LocationDescriptor } from 'history'; +import { url } from '../../../kibana_utils/public'; import { ManagementAppMountParams } from '../../../management/public'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { StartServicesAccessor } from '../../../../core/public'; + +import { AdvancedSettings, QUERY } from './advanced_settings'; import { ComponentRegistry } from '../types'; import './index.scss'; -import { UsageCollectionSetup } from '../../../usage_collection/public'; const title = i18n.translate('advancedSettings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', @@ -36,6 +39,18 @@ const readOnlyBadge = { iconType: 'glasses', }; +const redirectUrl = ({ + match, + location, +}: RouteChildrenProps<{ [QUERY]: string }>): LocationDescriptor => { + const search = url.addQueryParam(location.search, QUERY, match?.params[QUERY]); + + return { + pathname: '/', + search, + }; +}; + export async function mountManagementSection( getStartServices: StartServicesAccessor, params: ManagementAppMountParams, @@ -56,8 +71,11 @@ export async function mountManagementSection( - + {/* TODO: remove route param (`query`) in 7.13 */} + {(props) => } + { test('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { @@ -57,3 +57,78 @@ describe('encodeQuery', () => { }); }); }); + +describe('addQueryParam', () => { + const sampleParams = '?myNumber=23&myString=test&myValue=&myBoolean=false'; + + describe('setting values', () => { + it('should perserve other values', () => { + expect(addQueryParam(sampleParams, 'myNewValue', 'test')).toEqual( + 'myBoolean=false&myNumber=23&myString=test&myValue=&myNewValue=test' + ); + }); + + it('should set boolean values', () => { + expect(addQueryParam('', 'myBoolean', 'false')).toEqual('myBoolean=false'); + expect(addQueryParam('', 'myBoolean', 'true')).toEqual('myBoolean=true'); + }); + + it('should set string values', () => { + expect(addQueryParam('', 'myString', 'test')).toEqual('myString=test'); + expect(addQueryParam('', 'myString', '')).toEqual('myString='); + }); + + it('should set number values', () => { + expect(addQueryParam('', 'myNumber', '23')).toEqual('myNumber=23'); + expect(addQueryParam('', 'myNumber', '0')).toEqual('myNumber=0'); + }); + }); + + describe('changing values', () => { + it('should perserve other values', () => { + expect(addQueryParam(sampleParams, 'myBoolean', 'true')).toEqual( + 'myBoolean=true&myNumber=23&myString=test&myValue=' + ); + }); + + it('should change boolean value', () => { + expect(addQueryParam('?myBoolean=true', 'myBoolean', 'false')).toEqual('myBoolean=false'); + expect(addQueryParam('?myBoolean=false', 'myBoolean', 'true')).toEqual('myBoolean=true'); + }); + + it('should change string values', () => { + expect(addQueryParam('?myString=initial', 'myString', 'test')).toEqual('myString=test'); + expect(addQueryParam('?myString=initial', 'myString', '')).toEqual('myString='); + }); + + it('should change number values', () => { + expect(addQueryParam('?myNumber=1', 'myNumber', '23')).toEqual('myNumber=23'); + expect(addQueryParam('?myNumber=1', 'myNumber', '0')).toEqual('myNumber=0'); + }); + }); + + describe('deleting values', () => { + it('should perserve other values', () => { + expect(addQueryParam(sampleParams, 'myNumber')).toEqual( + 'myBoolean=false&myString=test&myValue=' + ); + }); + + it('should delete empty values', () => { + expect(addQueryParam('?myValue=', 'myValue')).toEqual(''); + }); + + it('should delete boolean values', () => { + expect(addQueryParam('?myBoolean=false', 'myBoolean')).toEqual(''); + expect(addQueryParam('?myBoolean=true', 'myBoolean')).toEqual(''); + }); + + it('should delete string values', () => { + expect(addQueryParam('?myString=test', 'myString')).toEqual(''); + }); + + it('should delete number values', () => { + expect(addQueryParam('?myNumber=23', 'myNumber')).toEqual(''); + }); + }); +}); diff --git a/src/plugins/kibana_utils/common/url/encode_uri_query.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.ts index ebc267c352227..c5fae36b13459 100644 --- a/src/plugins/kibana_utils/common/url/encode_uri_query.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { ParsedQuery } from 'query-string'; +import { ParsedQuery, parse, stringify } from 'query-string'; import { transform } from 'lodash'; /** @@ -32,15 +32,38 @@ export function encodeUriQuery(val: string, pctEncodeSpaces = false) { export const encodeQuery = ( query: ParsedQuery, - encodeFunction: (val: string, pctEncodeSpaces?: boolean) => string = encodeUriQuery -) => - transform(query, (result: any, value, key) => { + encodeFunction: (val: string, pctEncodeSpaces?: boolean) => string = encodeUriQuery, + pctEncodeSpaces = true +): ParsedQuery => + transform(query, (result, value, key) => { if (key) { const singleValue = Array.isArray(value) ? value.join(',') : value; result[key] = encodeFunction( singleValue === undefined || singleValue === null ? '' : singleValue, - true + pctEncodeSpaces ); } }); + +/** + * Method to help modify url query params. + * + * @param params + * @param key + * @param value + */ +export const addQueryParam = (params: string, key: string, value?: string) => { + const queryParams = parse(params); + + if (value !== undefined) { + queryParams[key] = value; + } else { + delete queryParams[key]; + } + + return stringify(encodeQuery(queryParams, undefined, false), { + sort: false, + encode: false, + }); +}; diff --git a/src/plugins/kibana_utils/common/url/index.ts b/src/plugins/kibana_utils/common/url/index.ts index b2705a45fc4d7..fffef45dbe897 100644 --- a/src/plugins/kibana_utils/common/url/index.ts +++ b/src/plugins/kibana_utils/common/url/index.ts @@ -6,9 +6,10 @@ * Public License, v 1. */ -import { encodeUriQuery, encodeQuery } from './encode_uri_query'; +import { encodeUriQuery, encodeQuery, addQueryParam } from './encode_uri_query'; export const url = { encodeQuery, encodeUriQuery, + addQueryParam, }; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 3b69544bd63db..ede209b772a4e 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -38,7 +38,7 @@ interface Props { isSecurityExampleEnabled: () => boolean; showAppliesSettingMessage: boolean; enableSaving: boolean; - query?: any; + query?: { text: string }; toasts: ToastsStart; } @@ -51,34 +51,42 @@ interface State { } export class TelemetryManagementSection extends Component { - state: State = { - processing: false, - showExample: false, - showSecurityExample: false, - queryMatches: null, - enabled: this.props.telemetryService.getIsOptedIn() || false, - }; + constructor(props: Props) { + super(props); + + this.state = { + processing: false, + showExample: false, + showSecurityExample: false, + queryMatches: props.query ? this.checkQueryMatch(props.query) : null, + enabled: this.props.telemetryService.getIsOptedIn() || false, + }; + } UNSAFE_componentWillReceiveProps(nextProps: Props) { const { query } = nextProps; + const queryMatches = this.checkQueryMatch(query); - const searchTerm = (query.text || '').toLowerCase(); - const searchTermMatches = - this.props.telemetryService.getCanChangeOptInStatus() && - SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0); - - if (searchTermMatches !== this.state.queryMatches) { + if (queryMatches !== this.state.queryMatches) { this.setState( { - queryMatches: searchTermMatches, + queryMatches, }, () => { - this.props.onQueryMatchChange(searchTermMatches); + this.props.onQueryMatchChange(queryMatches); } ); } } + checkQueryMatch(query?: { text: string }): boolean { + const searchTerm = (query?.text ?? '').toLowerCase(); + return ( + this.props.telemetryService.getCanChangeOptInStatus() && + SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0) + ); + } + render() { const { telemetryService, isSecurityExampleEnabled } = this.props; const { showExample, showSecurityExample, queryMatches, enabled, processing } = this.state; From 466d83c6d18d679fdd6ac0bf4fb63fe2ee091394 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 20 Jan 2021 13:15:59 -0500 Subject: [PATCH 13/28] [CI] [TeamCity] Enable job triggers in TeamCity (#88869) --- .teamcity/src/Common.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.teamcity/src/Common.kt b/.teamcity/src/Common.kt index 35bc881b88967..2e5357541bfe7 100644 --- a/.teamcity/src/Common.kt +++ b/.teamcity/src/Common.kt @@ -4,7 +4,7 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext const val ENABLE_REPORTING = false // If set to false, jobs with triggers (scheduled, on commit, etc) will be paused -const val ENABLE_TRIGGERS = false +const val ENABLE_TRIGGERS = true fun getProjectBranch(): String { return DslContext.projectName From edb338a8ad4c3917338eb7b274878146015934bf Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 20 Jan 2021 19:25:32 +0100 Subject: [PATCH 14/28] Add SO import hook / warnings API (#87996) * initial commit * adapt client-side signatures * more type fixes * adapt api IT asserts * fix some unit tests * fix more test usages * fix integration tests * fix FT test assertions * fix FT test assertions * add FTR API integ test suite * create the plugin_api_integration test suite * adapt and fix flyout tests * update documentation * update generated doc * add unit tests for `executeImportHooks` * wire resolve_import_errors and add unit tests * move hooks registration to SO type API * update generated doc * design integration * update generated doc * Add FTR tests for import warnings * deletes plugins api integ tests * self review * move onImport to management definition * update license header * rename actionUrl to actionPath --- .../core/public/kibana-plugin-core-public.md | 3 + ...simportactionrequiredwarning.actionpath.md | 13 ++ ...importactionrequiredwarning.buttonlabel.md | 13 ++ ...savedobjectsimportactionrequiredwarning.md | 25 ++++ ...ectsimportactionrequiredwarning.message.md | 13 ++ ...objectsimportactionrequiredwarning.type.md | 11 ++ ...-core-public.savedobjectsimportresponse.md | 1 + ...lic.savedobjectsimportresponse.warnings.md | 11 ++ ...-public.savedobjectsimportsimplewarning.md | 21 +++ ...savedobjectsimportsimplewarning.message.md | 13 ++ ...ic.savedobjectsimportsimplewarning.type.md | 11 ++ ...n-core-public.savedobjectsimportwarning.md | 15 ++ .../core/server/kibana-plugin-core-server.md | 5 + ...simportactionrequiredwarning.actionpath.md | 13 ++ ...importactionrequiredwarning.buttonlabel.md | 13 ++ ...savedobjectsimportactionrequiredwarning.md | 25 ++++ ...ectsimportactionrequiredwarning.message.md | 13 ++ ...objectsimportactionrequiredwarning.type.md | 11 ++ ...ugin-core-server.savedobjectsimporthook.md | 17 +++ ...ore-server.savedobjectsimporthookresult.md | 20 +++ ...r.savedobjectsimporthookresult.warnings.md | 13 ++ ...-core-server.savedobjectsimportresponse.md | 1 + ...ver.savedobjectsimportresponse.warnings.md | 11 ++ ...-server.savedobjectsimportsimplewarning.md | 21 +++ ...savedobjectsimportsimplewarning.message.md | 13 ++ ...er.savedobjectsimportsimplewarning.type.md | 11 ++ ...n-core-server.savedobjectsimportwarning.md | 15 ++ ...er.savedobjectstypemanagementdefinition.md | 1 + ...bjectstypemanagementdefinition.onimport.md | 52 +++++++ src/core/public/http/base_path.mock.ts | 28 ++++ src/core/public/http/http_service.mock.ts | 2 + src/core/public/index.ts | 3 + src/core/public/public.api.md | 21 +++ src/core/public/saved_objects/index.ts | 3 + src/core/server/index.ts | 5 + .../import/import_saved_objects.test.ts | 110 +++++++++++--- .../import/import_saved_objects.ts | 15 +- src/core/server/saved_objects/import/index.ts | 5 + .../import/lib/execute_import_hooks.test.ts | 135 ++++++++++++++++++ .../import/lib/execute_import_hooks.ts | 42 ++++++ .../server/saved_objects/import/lib/index.ts | 1 + .../import/resolve_import_errors.test.ts | 134 +++++++++++++---- .../import/resolve_import_errors.ts | 13 ++ .../import/saved_objects_importer.ts | 13 ++ src/core/server/saved_objects/import/types.ts | 70 +++++++++ src/core/server/saved_objects/index.ts | 5 + .../routes/integration_tests/import.test.ts | 10 +- .../resolve_import_errors.test.ts | 8 +- .../saved_objects/saved_objects_service.ts | 1 + src/core/server/saved_objects/types.ts | 46 ++++++ src/core/server/server.api.md | 30 ++++ .../public/lib/import_file.ts | 10 +- .../lib/process_import_response.test.ts | 40 +++++- .../public/lib/process_import_response.ts | 3 + .../__snapshots__/flyout.test.tsx.snap | 12 ++ .../objects_table/components/flyout.test.tsx | 4 +- .../objects_table/components/flyout.tsx | 16 ++- .../components/import_summary.test.tsx | 87 +++++++---- .../components/import_summary.tsx | 132 +++++++++++++---- .../objects_table/saved_objects_table.tsx | 1 + .../apis/saved_objects/import.ts | 5 + .../saved_objects/resolve_import_errors.ts | 9 +- .../management/saved_objects_page.ts | 16 +++ test/plugin_functional/config.ts | 1 + .../plugins/saved_object_hooks/kibana.json | 8 ++ .../plugins/saved_object_hooks/package.json | 14 ++ .../saved_object_hooks/server/index.ts | 11 ++ .../saved_object_hooks/server/plugin.ts | 64 +++++++++ .../plugins/saved_object_hooks/tsconfig.json | 18 +++ .../exports/_import_both_types.ndjson | 2 + .../exports/_import_type_1.ndjson | 1 + .../exports/_import_type_2.ndjson | 1 + .../import_warnings.ts | 74 ++++++++++ .../saved_objects_management/index.ts | 15 ++ .../components/copy_to_space_flyout.test.tsx | 10 ++ .../lib/copy_to_spaces/copy_to_spaces.test.ts | 2 + .../resolve_copy_conflicts.test.ts | 2 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 79 files changed, 1538 insertions(+), 129 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.warnings.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportwarning.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthook.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.warnings.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportwarning.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md create mode 100644 src/core/public/http/base_path.mock.ts create mode 100644 src/core/server/saved_objects/import/lib/execute_import_hooks.test.ts create mode 100644 src/core/server/saved_objects/import/lib/execute_import_hooks.ts create mode 100644 test/plugin_functional/plugins/saved_object_hooks/kibana.json create mode 100644 test/plugin_functional/plugins/saved_object_hooks/package.json create mode 100644 test/plugin_functional/plugins/saved_object_hooks/server/index.ts create mode 100644 test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts create mode 100644 test/plugin_functional/plugins/saved_object_hooks/tsconfig.json create mode 100644 test/plugin_functional/test_suites/saved_objects_management/exports/_import_both_types.ndjson create mode 100644 test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_1.ndjson create mode 100644 test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_2.ndjson create mode 100644 test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts create mode 100644 test/plugin_functional/test_suites/saved_objects_management/index.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index bfe787f3793d7..efd499823ffad 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -109,12 +109,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | | | [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) | A warning meant to notify that a specific user action is required to finalize the import of some type of object. The actionUrl must be a path relative to the basePath, and not include it. | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) | A simple informative warning that will be displayed to the user. | | [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) | Represents a successful import. | | [SavedObjectsImportUnknownError](./kibana-plugin-core-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | @@ -163,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | +| [SavedObjectsImportWarning](./kibana-plugin-core-public.savedobjectsimportwarning.md) | Composite type of all the possible types of import warnings.See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) for more details. | | [SavedObjectsNamespaceType](./kibana-plugin-core-public.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. | | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-core-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed start. | | [StringValidation](./kibana-plugin-core-public.stringvalidation.md) | Allows regex objects or a regex string | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md new file mode 100644 index 0000000000000..120a9d5f3386c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md) + +## SavedObjectsImportActionRequiredWarning.actionPath property + +The path (without the basePath) that the user should be redirect to to address this warning. + +Signature: + +```typescript +actionPath: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md new file mode 100644 index 0000000000000..ae7daba4860ef --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [buttonLabel](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md) + +## SavedObjectsImportActionRequiredWarning.buttonLabel property + +An optional label to use for the link button. If unspecified, a default label will be used. + +Signature: + +```typescript +buttonLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md new file mode 100644 index 0000000000000..26d734c39c918 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) + +## SavedObjectsImportActionRequiredWarning interface + +A warning meant to notify that a specific user action is required to finalize the import of some type of object. + + The `actionUrl` must be a path relative to the basePath, and not include it. + +Signature: + +```typescript +export interface SavedObjectsImportActionRequiredWarning +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to to address this warning. | +| [buttonLabel](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | An optional label to use for the link button. If unspecified, a default label will be used. | +| [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | +| [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md) | 'action_required' | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md new file mode 100644 index 0000000000000..c0f322892577e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md) + +## SavedObjectsImportActionRequiredWarning.message property + +The translated message to display to the user. + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md new file mode 100644 index 0000000000000..ee88f6a0d5d85 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md) + +## SavedObjectsImportActionRequiredWarning.type property + +Signature: + +```typescript +type: 'action_required'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md index 2c0b691c9d66e..3be800498a9b7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md @@ -20,4 +20,5 @@ export interface SavedObjectsImportResponse | [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | +| [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning[] | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.warnings.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.warnings.md new file mode 100644 index 0000000000000..2e55a2e30f9cb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.warnings.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) > [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md) + +## SavedObjectsImportResponse.warnings property + +Signature: + +```typescript +warnings: SavedObjectsImportWarning[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md new file mode 100644 index 0000000000000..4d6d984777c80 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) + +## SavedObjectsImportSimpleWarning interface + +A simple informative warning that will be displayed to the user. + +Signature: + +```typescript +export interface SavedObjectsImportSimpleWarning +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | +| [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md) | 'simple' | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md new file mode 100644 index 0000000000000..42c94e14e3d28 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) > [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md) + +## SavedObjectsImportSimpleWarning.message property + +The translated message to display to the user + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md new file mode 100644 index 0000000000000..86a4cbfa434e7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) > [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md) + +## SavedObjectsImportSimpleWarning.type property + +Signature: + +```typescript +type: 'simple'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportwarning.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportwarning.md new file mode 100644 index 0000000000000..a9a9a70774970 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportwarning.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportWarning](./kibana-plugin-core-public.savedobjectsimportwarning.md) + +## SavedObjectsImportWarning type + +Composite type of all the possible types of import warnings. + +See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) for more details. + +Signature: + +```typescript +export declare type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 06c7983f89a78..7daf5d086d9e4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -168,13 +168,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) | | | [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | | [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | | +| [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) | A warning meant to notify that a specific user action is required to finalize the import of some type of object. The actionUrl must be a path relative to the basePath, and not include it. | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) | Represents a failure to import. | +| [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md) | Result from a [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) | Options to control the import operation. | | [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) | A simple informative warning that will be displayed to the user. | | [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) | Represents a successful import. | | [SavedObjectsImportUnknownError](./kibana-plugin-core-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | @@ -295,6 +298,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | +| [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. | +| [SavedObjectsImportWarning](./kibana-plugin-core-server.savedobjectsimportwarning.md) | Composite type of all the possible types of import warnings.See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) for more details. | | [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. | | [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) | Describes Saved Object documents from Kibana < 7.0.0 which don't have a references root property defined. This type should only be used in migrations. | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md new file mode 100644 index 0000000000000..4ec70301d2ebe --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md) + +## SavedObjectsImportActionRequiredWarning.actionPath property + +The path (without the basePath) that the user should be redirect to to address this warning. + +Signature: + +```typescript +actionPath: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md new file mode 100644 index 0000000000000..7fb5d53c487af --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [buttonLabel](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md) + +## SavedObjectsImportActionRequiredWarning.buttonLabel property + +An optional label to use for the link button. If unspecified, a default label will be used. + +Signature: + +```typescript +buttonLabel?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md new file mode 100644 index 0000000000000..ba1e905344af1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) + +## SavedObjectsImportActionRequiredWarning interface + +A warning meant to notify that a specific user action is required to finalize the import of some type of object. + + The `actionUrl` must be a path relative to the basePath, and not include it. + +Signature: + +```typescript +export interface SavedObjectsImportActionRequiredWarning +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to to address this warning. | +| [buttonLabel](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | An optional label to use for the link button. If unspecified, a default label will be used. | +| [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | +| [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md) | 'action_required' | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md new file mode 100644 index 0000000000000..1ab9afd4bad99 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md) + +## SavedObjectsImportActionRequiredWarning.message property + +The translated message to display to the user. + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md new file mode 100644 index 0000000000000..d8f22ef17d8f0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md) + +## SavedObjectsImportActionRequiredWarning.type property + +Signature: + +```typescript +type: 'action_required'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthook.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthook.md new file mode 100644 index 0000000000000..8d50ef94577de --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthook.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) + +## SavedObjectsImportHook type + +A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type. + +Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. + + The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. + +Signature: + +```typescript +export declare type SavedObjectsImportHook = (objects: Array>) => SavedObjectsImportHookResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md new file mode 100644 index 0000000000000..9756ce7fac350 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md) + +## SavedObjectsImportHookResult interface + +Result from a [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) + +Signature: + +```typescript +export interface SavedObjectsImportHookResult +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [warnings](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md) | SavedObjectsImportWarning[] | An optional list of warnings to display in the UI when the import succeeds. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md new file mode 100644 index 0000000000000..682b384f8d363 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md) > [warnings](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md) + +## SavedObjectsImportHookResult.warnings property + +An optional list of warnings to display in the UI when the import succeeds. + +Signature: + +```typescript +warnings?: SavedObjectsImportWarning[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md index 94d24e946b5bd..55f651197490f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md @@ -20,4 +20,5 @@ export interface SavedObjectsImportResponse | [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | +| [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.warnings.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.warnings.md new file mode 100644 index 0000000000000..88cccf7f527e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.warnings.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) > [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md) + +## SavedObjectsImportResponse.warnings property + +Signature: + +```typescript +warnings: SavedObjectsImportWarning[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md new file mode 100644 index 0000000000000..52d46e4f8db80 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) + +## SavedObjectsImportSimpleWarning interface + +A simple informative warning that will be displayed to the user. + +Signature: + +```typescript +export interface SavedObjectsImportSimpleWarning +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | +| [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md) | 'simple' | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md new file mode 100644 index 0000000000000..1e3ac7ec11365 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) > [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md) + +## SavedObjectsImportSimpleWarning.message property + +The translated message to display to the user + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md new file mode 100644 index 0000000000000..660b1b39d6c39 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) > [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md) + +## SavedObjectsImportSimpleWarning.type property + +Signature: + +```typescript +type: 'simple'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportwarning.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportwarning.md new file mode 100644 index 0000000000000..257751f16601d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportwarning.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportWarning](./kibana-plugin-core-server.savedobjectsimportwarning.md) + +## SavedObjectsImportWarning type + +Composite type of all the possible types of import warnings. + +See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) for more details. + +Signature: + +```typescript +export declare type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index 9d87e51767caa..92b6ddf29b8ec 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -22,4 +22,5 @@ export interface SavedObjectsTypeManagementDefinition | [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<any>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | | [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | | [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | +| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md new file mode 100644 index 0000000000000..55733ca5d4443 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md @@ -0,0 +1,52 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) + +## SavedObjectsTypeManagementDefinition.onImport property + +An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type. + +Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. + +Signature: + +```typescript +onImport?: SavedObjectsImportHook; +``` + +## Example + +Registering a hook displaying a warning about a specific type of object + +```ts +// src/plugins/my_plugin/server/plugin.ts +import { myType } from './saved_objects'; + +export class Plugin() { + setup: (core: CoreSetup) => { + core.savedObjects.registerType({ + ...myType, + management: { + ...myType.management, + onImport: (objects) => { + if(someActionIsNeeded(objects)) { + return { + warnings: [ + { + type: 'action_required', + message: 'Objects need to be manually enabled after import', + actionPath: '/app/my-app/require-activation', + }, + ] + } + } + return {}; + } + }, + }); + } +} + +``` + messages returned in the warnings are user facing and must be translated. + diff --git a/src/core/public/http/base_path.mock.ts b/src/core/public/http/base_path.mock.ts new file mode 100644 index 0000000000000..851ad1ffca440 --- /dev/null +++ b/src/core/public/http/base_path.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { IBasePath } from './types'; + +const createBasePathMock = ({ + publicBaseUrl = '/', + serverBasePath = '/', +}: { publicBaseUrl?: string; serverBasePath?: string } = {}) => { + const mock: jest.Mocked = { + prepend: jest.fn(), + get: jest.fn(), + remove: jest.fn(), + publicBaseUrl, + serverBasePath, + }; + + return mock; +}; + +export const basePathMock = { + create: createBasePathMock, +}; diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index bbc412461c480..c00773b510556 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -11,6 +11,7 @@ import { HttpService } from './http_service'; import { HttpSetup } from './types'; import { BehaviorSubject } from 'rxjs'; import { BasePath } from './base_path'; +import { basePathMock } from './base_path.mock'; export type HttpSetupMock = jest.Mocked & { basePath: BasePath; @@ -54,4 +55,5 @@ export const httpServiceMock = { create: createMock, createSetupContract: createServiceMock, createStartContract: createServiceMock, + createBasePath: basePathMock.create, }; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 784def847f1ba..66dd4f3028aaf 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -130,6 +130,9 @@ export { SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, + SavedObjectsImportSimpleWarning, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportWarning, } from './saved_objects'; export { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 5c0b2a45abd46..da818470133cd 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1198,6 +1198,15 @@ export interface SavedObjectsFindResponsePublic extends SavedObject total: number; } +// @public +export interface SavedObjectsImportActionRequiredWarning { + actionPath: string; + buttonLabel?: string; + message: string; + // (undocumented) + type: 'action_required'; +} + // @public export interface SavedObjectsImportAmbiguousConflictError { // (undocumented) @@ -1257,6 +1266,8 @@ export interface SavedObjectsImportResponse { successCount: number; // (undocumented) successResults?: SavedObjectsImportSuccess[]; + // (undocumented) + warnings: SavedObjectsImportWarning[]; } // @public @@ -1278,6 +1289,13 @@ export interface SavedObjectsImportRetry { type: string; } +// @public +export interface SavedObjectsImportSimpleWarning { + message: string; + // (undocumented) + type: 'simple'; +} + // @public export interface SavedObjectsImportSuccess { // @deprecated (undocumented) @@ -1311,6 +1329,9 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @public +export type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning; + // @public export interface SavedObjectsMigrationVersion { // (undocumented) diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e83d2044a2d70..537ca0da088ff 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -35,6 +35,9 @@ export { SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, + SavedObjectsImportSimpleWarning, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportWarning, } from '../../server/types'; export { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 187c2afd17039..0eb246b4c978b 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -321,6 +321,11 @@ export { SavedObjectsImporter, ISavedObjectsImporter, SavedObjectsImportError, + SavedObjectsImportHook, + SavedObjectsImportHookResult, + SavedObjectsImportSimpleWarning, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportWarning, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index e675fb7ea1000..11d3df5faae53 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -18,6 +18,7 @@ import { savedObjectsClientMock } from '../../mocks'; import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects'; +import { SavedObjectsImportHook, SavedObjectsImportWarning } from './types'; import { collectSavedObjects, @@ -26,6 +27,7 @@ import { checkConflicts, checkOriginConflicts, createSavedObjects, + executeImportHooks, } from './lib'; jest.mock('./lib/collect_saved_objects'); @@ -34,6 +36,7 @@ jest.mock('./lib/validate_references'); jest.mock('./lib/check_conflicts'); jest.mock('./lib/check_origin_conflicts'); jest.mock('./lib/create_saved_objects'); +jest.mock('./lib/execute_import_hooks'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -61,6 +64,7 @@ describe('#importSavedObjectsFromStream', () => { pendingOverwrites: new Set(), }); getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] }); + getMockFn(executeImportHooks).mockResolvedValue([]); }); let readStream: Readable; @@ -70,14 +74,19 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = ( - createNewCopies: boolean = false, - getTypeImpl: (name: string) => any = (type: string) => + const setupOptions = ({ + createNewCopies = false, + getTypeImpl = (type: string) => ({ // other attributes aren't needed for the purposes of injecting metadata management: { icon: `${type}-icon` }, - } as any) - ): ImportSavedObjectsOptions => { + } as any), + importHooks = {}, + }: { + createNewCopies?: boolean; + getTypeImpl?: (name: string) => any; + importHooks?: Record; + } = {}): ImportSavedObjectsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); @@ -90,6 +99,7 @@ describe('#importSavedObjectsFromStream', () => { typeRegistry, namespace, createNewCopies, + importHooks, }; }; const createObject = ({ @@ -153,6 +163,31 @@ describe('#importSavedObjectsFromStream', () => { ); }); + test('executes import hooks', async () => { + const importHooks = { + foo: [jest.fn()], + }; + + const options = setupOptions({ importHooks }); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ + errors: [], + createdObjects: collectedObjects, + }); + + await importSavedObjectsFromStream(options); + + expect(executeImportHooks).toHaveBeenCalledWith({ + objects: collectedObjects, + importHooks, + }); + }); + describe('with createNewCopies disabled', () => { test('does not regenerate object IDs', async () => { const options = setupOptions(); @@ -256,7 +291,7 @@ describe('#importSavedObjectsFromStream', () => { describe('with createNewCopies enabled', () => { test('regenerates object IDs', async () => { - const options = setupOptions(true); + const options = setupOptions({ createNewCopies: true }); const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -269,7 +304,7 @@ describe('#importSavedObjectsFromStream', () => { }); test('does not check conflicts or check origin conflicts', async () => { - const options = setupOptions(true); + const options = setupOptions({ createNewCopies: true }); getMockFn(validateReferences).mockResolvedValue([]); await importSavedObjectsFromStream(options); @@ -278,7 +313,7 @@ describe('#importSavedObjectsFromStream', () => { }); test('creates saved objects', async () => { - const options = setupOptions(true); + const options = setupOptions({ createNewCopies: true }); const collectedObjects = [createObject()]; const errors = [createError(), createError()]; getMockFn(collectSavedObjects).mockResolvedValue({ @@ -313,7 +348,7 @@ describe('#importSavedObjectsFromStream', () => { const options = setupOptions(); const result = await importSavedObjectsFromStream(options); - expect(result).toEqual({ success: true, successCount: 0 }); + expect(result).toEqual({ success: true, successCount: 0, warnings: [] }); }); test('returns success=false if an error occurred', async () => { @@ -325,7 +360,33 @@ describe('#importSavedObjectsFromStream', () => { }); const result = await importSavedObjectsFromStream(options); - expect(result).toEqual({ success: false, successCount: 0, errors: [expect.any(Object)] }); + expect(result).toEqual({ + success: false, + successCount: 0, + errors: [expect.any(Object)], + warnings: [], + }); + }); + + test('returns warnings from the import hooks', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ + errors: [], + createdObjects: collectedObjects, + }); + + const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }]; + getMockFn(executeImportHooks).mockResolvedValue(warnings); + + const result = await importSavedObjectsFromStream(options); + + expect(result.warnings).toEqual(warnings); }); describe('handles a mix of successes and errors and injects metadata', () => { @@ -389,12 +450,13 @@ describe('#importSavedObjectsFromStream', () => { successCount: 3, successResults, errors: errorResults, + warnings: [], }); }); test('with createNewCopies enabled', async () => { // however, we include it here for posterity - const options = setupOptions(true); + const options = setupOptions({ createNewCopies: true }); getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects }); const result = await importSavedObjectsFromStream(options); @@ -410,6 +472,7 @@ describe('#importSavedObjectsFromStream', () => { successCount: 3, successResults, errors: errorResults, + warnings: [], }); }); }); @@ -418,15 +481,18 @@ describe('#importSavedObjectsFromStream', () => { const obj1 = createObject({ type: 'foo' }); const obj2 = createObject({ type: 'bar', title: 'bar-title' }); - const options = setupOptions(false, (type) => { - if (type === 'foo') { + const options = setupOptions({ + createNewCopies: false, + getTypeImpl: (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } return { - management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + management: { icon: `${type}-icon` }, }; - } - return { - management: { icon: `${type}-icon` }, - }; + }, }); getMockFn(checkConflicts).mockResolvedValue({ @@ -456,6 +522,7 @@ describe('#importSavedObjectsFromStream', () => { success: true, successCount: 2, successResults, + warnings: [], }); }); @@ -483,7 +550,12 @@ describe('#importSavedObjectsFromStream', () => { const result = await importSavedObjectsFromStream(options); const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); - expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + expect(result).toEqual({ + success: false, + successCount: 0, + errors: expectedErrors, + warnings: [], + }); }); }); }); diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index a788bcf47d321..9baef59dc162a 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -9,7 +9,11 @@ import { Readable } from 'stream'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { SavedObjectsClientContract } from '../types'; -import { SavedObjectsImportFailure, SavedObjectsImportResponse } from './types'; +import { + SavedObjectsImportFailure, + SavedObjectsImportResponse, + SavedObjectsImportHook, +} from './types'; import { validateReferences, checkOriginConflicts, @@ -17,6 +21,7 @@ import { checkConflicts, regenerateIds, collectSavedObjects, + executeImportHooks, } from './lib'; /** @@ -33,6 +38,8 @@ export interface ImportSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; /** The registry of all known saved object types */ typeRegistry: ISavedObjectTypeRegistry; + /** List of registered import hooks */ + importHooks: Record; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ @@ -52,6 +59,7 @@ export async function importSavedObjectsFromStream({ createNewCopies, savedObjectsClient, typeRegistry, + importHooks, namespace, }: ImportSavedObjectsOptions): Promise { let errorAccumulator: SavedObjectsImportFailure[] = []; @@ -147,10 +155,15 @@ export async function importSavedObjectsFromStream({ ...(attemptedOverwrite && { overwrite: true }), }; }); + const warnings = await executeImportHooks({ + objects: createSavedObjectsResult.createdObjects, + importHooks, + }); return { successCount: createSavedObjectsResult.createdObjects.length, success: errorAccumulator.length === 0, + warnings, ...(successResults.length && { successResults }), ...(errorResults.length && { errors: errorResults }), }; diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index 4cc2e6e83995b..0616c1277a3ac 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -19,5 +19,10 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsResolveImportErrorsOptions, SavedObjectsImportRetry, + SavedObjectsImportHook, + SavedObjectsImportHookResult, + SavedObjectsImportSimpleWarning, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportWarning, } from './types'; export { SavedObjectsImportError } from './errors'; diff --git a/src/core/server/saved_objects/import/lib/execute_import_hooks.test.ts b/src/core/server/saved_objects/import/lib/execute_import_hooks.test.ts new file mode 100644 index 0000000000000..ca769bc9ac4c1 --- /dev/null +++ b/src/core/server/saved_objects/import/lib/execute_import_hooks.test.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObject } from '../../types'; +import { SavedObjectsImportHookResult, SavedObjectsImportWarning } from '../types'; +import { executeImportHooks } from './execute_import_hooks'; + +const createObject = (type: string, id: string): SavedObject => ({ + type, + id, + attributes: {}, + references: [], +}); + +const createHook = ( + result: SavedObjectsImportHookResult | Promise = {} +) => jest.fn().mockReturnValue(result); + +const createWarning = (message: string): SavedObjectsImportWarning => ({ + type: 'simple', + message, +}); + +describe('executeImportHooks', () => { + it('invokes the hooks with the correct objects', async () => { + const foo1 = createObject('foo', '1'); + const foo2 = createObject('foo', '2'); + const bar1 = createObject('bar', '1'); + const objects = [foo1, bar1, foo2]; + + const fooHook = createHook(); + const barHook = createHook(); + + await executeImportHooks({ + objects, + importHooks: { + foo: [fooHook], + bar: [barHook], + }, + }); + + expect(fooHook).toHaveBeenCalledTimes(1); + expect(fooHook).toHaveBeenCalledWith([foo1, foo2]); + + expect(barHook).toHaveBeenCalledTimes(1); + expect(barHook).toHaveBeenCalledWith([bar1]); + }); + + it('handles multiple hooks per type', async () => { + const foo1 = createObject('foo', '1'); + const foo2 = createObject('foo', '2'); + const bar1 = createObject('bar', '1'); + const objects = [foo1, bar1, foo2]; + + const fooHook1 = createHook(); + const fooHook2 = createHook(); + + await executeImportHooks({ + objects, + importHooks: { + foo: [fooHook1, fooHook2], + }, + }); + + expect(fooHook1).toHaveBeenCalledTimes(1); + expect(fooHook1).toHaveBeenCalledWith([foo1, foo2]); + + expect(fooHook2).toHaveBeenCalledTimes(1); + expect(fooHook2).toHaveBeenCalledWith([foo1, foo2]); + }); + + it('does not call a hook if no object of its type is present', async () => { + const objects = [createObject('foo', '1'), createObject('foo', '2')]; + const hook = createHook(); + + await executeImportHooks({ + objects, + importHooks: { + bar: [hook], + }, + }); + + expect(hook).not.toHaveBeenCalled(); + }); + + it('returns the warnings returned by the hooks', async () => { + const foo1 = createObject('foo', '1'); + const bar1 = createObject('bar', '1'); + const objects = [foo1, bar1]; + + const fooWarning1 = createWarning('foo warning 1'); + const fooWarning2 = createWarning('foo warning 2'); + const barWarning = createWarning('bar warning'); + + const fooHook = createHook({ warnings: [fooWarning1, fooWarning2] }); + const barHook = createHook({ warnings: [barWarning] }); + + const warnings = await executeImportHooks({ + objects, + importHooks: { + foo: [fooHook], + bar: [barHook], + }, + }); + + expect(warnings).toEqual([fooWarning1, fooWarning2, barWarning]); + }); + + it('handles asynchronous hooks', async () => { + const foo1 = createObject('foo', '1'); + const bar1 = createObject('bar', '1'); + const objects = [foo1, bar1]; + + const fooWarning = createWarning('foo warning 1'); + const barWarning = createWarning('bar warning'); + + const fooHook = createHook(Promise.resolve({ warnings: [fooWarning] })); + const barHook = createHook(Promise.resolve({ warnings: [barWarning] })); + + const warnings = await executeImportHooks({ + objects, + importHooks: { + foo: [fooHook], + bar: [barHook], + }, + }); + + expect(warnings).toEqual([fooWarning, barWarning]); + }); +}); diff --git a/src/core/server/saved_objects/import/lib/execute_import_hooks.ts b/src/core/server/saved_objects/import/lib/execute_import_hooks.ts new file mode 100644 index 0000000000000..aff8bac1c17ca --- /dev/null +++ b/src/core/server/saved_objects/import/lib/execute_import_hooks.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObject } from '../../types'; +import { SavedObjectsImportHook, SavedObjectsImportWarning } from '../types'; + +interface ExecuteImportHooksOptions { + objects: SavedObject[]; + importHooks: Record; +} + +export const executeImportHooks = async ({ + objects, + importHooks, +}: ExecuteImportHooksOptions): Promise => { + const objsByType = splitByType(objects); + let warnings: SavedObjectsImportWarning[] = []; + + for (const [type, typeObjs] of Object.entries(objsByType)) { + const hooks = importHooks[type] ?? []; + for (const hook of hooks) { + const hookResult = await hook(typeObjs); + if (hookResult.warnings) { + warnings = [...warnings, ...hookResult.warnings]; + } + } + } + + return warnings; +}; + +const splitByType = (objects: SavedObject[]): Record => { + return objects.reduce((memo, obj) => { + memo[obj.type] = [...(memo[obj.type] ?? []), obj]; + return memo; + }, {} as Record); +}; diff --git a/src/core/server/saved_objects/import/lib/index.ts b/src/core/server/saved_objects/import/lib/index.ts index ceb301cd10181..64735f1d0daca 100644 --- a/src/core/server/saved_objects/import/lib/index.ts +++ b/src/core/server/saved_objects/import/lib/index.ts @@ -18,3 +18,4 @@ export { regenerateIds } from './regenerate_ids'; export { splitOverwrites } from './split_overwrites'; export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; export { validateRetries } from './validate_retries'; +export { executeImportHooks } from './execute_import_hooks'; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index 28b31d22a4de8..b4861a35266b3 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -15,9 +15,10 @@ import { SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectReference, + SavedObjectsImportWarning, } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { ISavedObjectTypeRegistry } from '..'; +import { ISavedObjectTypeRegistry, SavedObjectsImportHook } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { resolveSavedObjectsImportErrors, @@ -34,6 +35,7 @@ import { splitOverwrites, createSavedObjects, createObjectsFilter, + executeImportHooks, } from './lib'; jest.mock('./lib/validate_retries'); @@ -45,6 +47,7 @@ jest.mock('./lib/check_conflicts'); jest.mock('./lib/check_origin_conflicts'); jest.mock('./lib/split_overwrites'); jest.mock('./lib/create_saved_objects'); +jest.mock('./lib/execute_import_hooks'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -73,6 +76,7 @@ describe('#importSavedObjectsFromStream', () => { objectsToNotOverwrite: [], }); getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] }); + getMockFn(executeImportHooks).mockResolvedValue([]); }); let readStream: Readable; @@ -81,15 +85,21 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = ( - retries: SavedObjectsImportRetry[] = [], - createNewCopies: boolean = false, - getTypeImpl: (name: string) => any = (type: string) => + const setupOptions = ({ + retries = [], + createNewCopies = false, + getTypeImpl = (type: string) => ({ // other attributes aren't needed for the purposes of injecting metadata management: { icon: `${type}-icon` }, - } as any) - ): ResolveSavedObjectsImportErrorsOptions => { + } as any), + importHooks = {}, + }: { + retries?: SavedObjectsImportRetry[]; + createNewCopies?: boolean; + getTypeImpl?: (name: string) => any; + importHooks?: Record; + } = {}): ResolveSavedObjectsImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); @@ -101,6 +111,7 @@ describe('#importSavedObjectsFromStream', () => { retries, savedObjectsClient, typeRegistry, + importHooks, // namespace and createNewCopies don't matter, as they don't change the logic in this module, they just get passed to sub-module methods namespace, createNewCopies, @@ -148,7 +159,7 @@ describe('#importSavedObjectsFromStream', () => { describe('module calls', () => { test('validates retries', async () => { const retry = createRetry(); - const options = setupOptions([retry]); + const options = setupOptions({ retries: [retry] }); await resolveSavedObjectsImportErrors(options); expect(validateRetries).toHaveBeenCalledWith([retry]); @@ -156,7 +167,7 @@ describe('#importSavedObjectsFromStream', () => { test('creates objects filter', async () => { const retry = createRetry(); - const options = setupOptions([retry]); + const options = setupOptions({ retries: [retry] }); await resolveSavedObjectsImportErrors(options); expect(createObjectsFilter).toHaveBeenCalledWith([retry]); @@ -178,7 +189,7 @@ describe('#importSavedObjectsFromStream', () => { test('validates references', async () => { const retries = [createRetry()]; - const options = setupOptions(retries); + const options = setupOptions({ retries }); const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -195,6 +206,30 @@ describe('#importSavedObjectsFromStream', () => { ); }); + test('execute import hooks', async () => { + const importHooks = { + foo: [jest.fn()], + }; + const options = setupOptions({ importHooks }); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [], + createdObjects: collectedObjects, + }); + + await resolveSavedObjectsImportErrors(options); + + expect(executeImportHooks).toHaveBeenCalledWith({ + objects: collectedObjects, + importHooks, + }); + }); + test('uses `retries` to replace references of collected objects before validating', async () => { const object = createObject([{ type: 'bar-type', id: 'abc', name: 'some name' }]); const retries = [ @@ -203,7 +238,7 @@ describe('#importSavedObjectsFromStream', () => { replaceReferences: [{ type: 'bar-type', from: 'abc', to: 'def' }], }), ]; - const options = setupOptions(retries); + const options = setupOptions({ retries }); getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [object], @@ -226,7 +261,7 @@ describe('#importSavedObjectsFromStream', () => { test('checks conflicts', async () => { const createNewCopies = (Symbol() as unknown) as boolean; const retries = [createRetry()]; - const options = setupOptions(retries, createNewCopies); + const options = setupOptions({ retries, createNewCopies }); const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -248,7 +283,7 @@ describe('#importSavedObjectsFromStream', () => { test('gets import ID map for retries', async () => { const retries = [createRetry()]; const createNewCopies = (Symbol() as unknown) as boolean; - const options = setupOptions(retries, createNewCopies); + const options = setupOptions({ retries, createNewCopies }); const filteredObjects = [createObject()]; getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -264,7 +299,7 @@ describe('#importSavedObjectsFromStream', () => { test('splits objects to overwrite from those not to overwrite', async () => { const retries = [createRetry()]; - const options = setupOptions(retries); + const options = setupOptions({ retries }); const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -344,7 +379,7 @@ describe('#importSavedObjectsFromStream', () => { describe('with createNewCopies enabled', () => { test('regenerates object IDs', async () => { - const options = setupOptions([], true); + const options = setupOptions({ createNewCopies: true }); const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -357,7 +392,7 @@ describe('#importSavedObjectsFromStream', () => { }); test('creates saved objects', async () => { - const options = setupOptions([], true); + const options = setupOptions({ createNewCopies: true }); const errors = [createError(), createError(), createError()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], @@ -422,7 +457,7 @@ describe('#importSavedObjectsFromStream', () => { const options = setupOptions(); const result = await resolveSavedObjectsImportErrors(options); - expect(result).toEqual({ success: true, successCount: 0 }); + expect(result).toEqual({ success: true, successCount: 0, warnings: [] }); }); test('returns success=false if an error occurred', async () => { @@ -434,15 +469,40 @@ describe('#importSavedObjectsFromStream', () => { }); const result = await resolveSavedObjectsImportErrors(options); - expect(result).toEqual({ success: false, successCount: 0, errors: [expect.any(Object)] }); + expect(result).toEqual({ + success: false, + successCount: 0, + errors: [expect.any(Object)], + warnings: [], + }); + }); + + test('executes import hooks', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [], + createdObjects: collectedObjects, + }); + const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }]; + getMockFn(executeImportHooks).mockResolvedValue(warnings); + + const result = await resolveSavedObjectsImportErrors(options); + + expect(result.warnings).toEqual(warnings); }); test('handles a mix of successes and errors and injects metadata', async () => { const error1 = createError(); const error2 = createError(); - const options = setupOptions([ - { type: error2.type, id: error2.id, overwrite: true, replaceReferences: [] }, - ]); + const options = setupOptions({ + retries: [{ type: error2.type, id: error2.id, overwrite: true, replaceReferences: [] }], + }); const obj1 = createObject(); const tmp = createObject(); const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id }; @@ -483,22 +543,30 @@ describe('#importSavedObjectsFromStream', () => { { ...error1, meta: { ...error1.meta, icon: `${error1.type}-icon` } }, { ...error2, meta: { ...error2.meta, icon: `${error2.type}-icon` }, overwrite: true }, ]; - expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); + expect(result).toEqual({ + success: false, + successCount: 3, + successResults, + errors, + warnings: [], + }); }); test('uses `type.management.getTitle` to resolve the titles', async () => { const obj1 = createObject([], { type: 'foo' }); const obj2 = createObject([], { type: 'bar', title: 'bar-title' }); - const options = setupOptions([], false, (type) => { - if (type === 'foo') { + const options = setupOptions({ + getTypeImpl: (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } return { - management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + management: { icon: `${type}-icon` }, }; - } - return { - management: { icon: `${type}-icon` }, - }; + }, }); getMockFn(checkConflicts).mockResolvedValue({ @@ -532,6 +600,7 @@ describe('#importSavedObjectsFromStream', () => { success: true, successCount: 2, successResults, + warnings: [], }); }); @@ -555,7 +624,12 @@ describe('#importSavedObjectsFromStream', () => { const result = await resolveSavedObjectsImportErrors(options); const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); - expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + expect(result).toEqual({ + success: false, + successCount: 0, + errors: expectedErrors, + warnings: [], + }); }); }); }); diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 5bb4c79e34cd4..4526eefe467d8 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -11,6 +11,7 @@ import { SavedObject, SavedObjectsClientContract, SavedObjectsImportRetry } from import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { SavedObjectsImportFailure, + SavedObjectsImportHook, SavedObjectsImportResponse, SavedObjectsImportSuccess, } from './types'; @@ -24,6 +25,7 @@ import { createSavedObjects, getImportIdMapForRetries, checkConflicts, + executeImportHooks, } from './lib'; /** @@ -38,6 +40,8 @@ export interface ResolveSavedObjectsImportErrorsOptions { savedObjectsClient: SavedObjectsClientContract; /** The registry of all known saved object types */ typeRegistry: ISavedObjectTypeRegistry; + /** List of registered import hooks */ + importHooks: Record; /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; /** if specified, will import in given namespace */ @@ -58,6 +62,7 @@ export async function resolveSavedObjectsImportErrors({ retries, savedObjectsClient, typeRegistry, + importHooks, namespace, createNewCopies, }: ResolveSavedObjectsImportErrorsOptions): Promise { @@ -146,6 +151,7 @@ export async function resolveSavedObjectsImportErrors({ // Bulk create in two batches, overwrites and non-overwrites let successResults: SavedObjectsImportSuccess[] = []; + let successObjects: SavedObject[] = []; const accumulatedErrors = [...errorAccumulator]; const bulkCreateObjects = async ( objects: Array>, @@ -162,6 +168,7 @@ export async function resolveSavedObjectsImportErrors({ const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( createSavedObjectsParams ); + successObjects = [...successObjects, ...createdObjects]; errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; successCount += createdObjects.length; successResults = [ @@ -200,9 +207,15 @@ export async function resolveSavedObjectsImportErrors({ }; }); + const warnings = await executeImportHooks({ + objects: successObjects, + importHooks, + }); + return { successCount, success: errorAccumulator.length === 0, + warnings, ...(successResults.length && { successResults }), ...(errorResults.length && { errors: errorResults }), }; diff --git a/src/core/server/saved_objects/import/saved_objects_importer.ts b/src/core/server/saved_objects/import/saved_objects_importer.ts index 77f4afd519cca..94568cb336634 100644 --- a/src/core/server/saved_objects/import/saved_objects_importer.ts +++ b/src/core/server/saved_objects/import/saved_objects_importer.ts @@ -15,6 +15,7 @@ import { SavedObjectsImportResponse, SavedObjectsImportOptions, SavedObjectsResolveImportErrorsOptions, + SavedObjectsImportHook, } from './types'; /** @@ -29,6 +30,7 @@ export class SavedObjectsImporter { readonly #savedObjectsClient: SavedObjectsClientContract; readonly #typeRegistry: ISavedObjectTypeRegistry; readonly #importSizeLimit: number; + readonly #importHooks: Record; constructor({ savedObjectsClient, @@ -42,6 +44,15 @@ export class SavedObjectsImporter { this.#savedObjectsClient = savedObjectsClient; this.#typeRegistry = typeRegistry; this.#importSizeLimit = importSizeLimit; + this.#importHooks = typeRegistry.getAllTypes().reduce((hooks, type) => { + if (type.management?.onImport) { + return { + ...hooks, + [type.name]: [type.management.onImport], + }; + } + return hooks; + }, {} as Record); } /** @@ -64,6 +75,7 @@ export class SavedObjectsImporter { objectLimit: this.#importSizeLimit, savedObjectsClient: this.#savedObjectsClient, typeRegistry: this.#typeRegistry, + importHooks: this.#importHooks, }); } @@ -87,6 +99,7 @@ export class SavedObjectsImporter { objectLimit: this.#importSizeLimit, savedObjectsClient: this.#savedObjectsClient, typeRegistry: this.#typeRegistry, + importHooks: this.#importHooks, }); } } diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 8a159824ec7a4..bbd814eb9b614 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -142,6 +142,7 @@ export interface SavedObjectsImportResponse { success: boolean; successCount: number; successResults?: SavedObjectsImportSuccess[]; + warnings: SavedObjectsImportWarning[]; errors?: SavedObjectsImportFailure[]; } @@ -176,3 +177,72 @@ export interface SavedObjectsResolveImportErrorsOptions { } export type CreatedObject = SavedObject & { destinationId?: string }; + +/** + * A simple informative warning that will be displayed to the user. + * + * @public + */ +export interface SavedObjectsImportSimpleWarning { + type: 'simple'; + /** The translated message to display to the user */ + message: string; +} + +/** + * A warning meant to notify that a specific user action is required to finalize the import + * of some type of object. + * + * @remark The `actionUrl` must be a path relative to the basePath, and not include it. + * + * @public + */ +export interface SavedObjectsImportActionRequiredWarning { + type: 'action_required'; + /** The translated message to display to the user. */ + message: string; + /** The path (without the basePath) that the user should be redirect to to address this warning. */ + actionPath: string; + /** An optional label to use for the link button. If unspecified, a default label will be used. */ + buttonLabel?: string; +} + +/** + * Composite type of all the possible types of import warnings. + * + * See {@link SavedObjectsImportSimpleWarning} and {@link SavedObjectsImportActionRequiredWarning} + * for more details. + * + * @public + */ +export type SavedObjectsImportWarning = + | SavedObjectsImportSimpleWarning + | SavedObjectsImportActionRequiredWarning; + +/** + * Result from a {@link SavedObjectsImportHook | import hook} + * + * @public + */ +export interface SavedObjectsImportHookResult { + /** + * An optional list of warnings to display in the UI when the import succeeds. + */ + warnings?: SavedObjectsImportWarning[]; +} + +/** + * A hook associated with a specific saved object type, that will be invoked during + * the import process. The hook will have access to the objects of the registered type. + * + * Currently, the only supported feature for import hooks is to return warnings to be displayed + * in the UI when the import succeeds. + * + * @remark The only interactions the hook can have with the import process is via the hook's + * response. Mutating the objects inside the hook's code will have no effect. + * + * @public + */ +export type SavedObjectsImportHook = ( + objects: Array> +) => SavedObjectsImportHookResult | Promise; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index ed641ca2add2a..57dee5cd51f1d 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -23,6 +23,11 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsResolveImportErrorsOptions, SavedObjectsImportError, + SavedObjectsImportHook, + SavedObjectsImportHookResult, + SavedObjectsImportSimpleWarning, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportWarning, } from './import'; export { diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index af1e3257479a5..af4f57f30f968 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -101,7 +101,7 @@ describe(`POST ${URL}`, () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 0 }); + expect(result.body).toEqual({ success: true, successCount: 0, warnings: [] }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({ request: expect.anything(), @@ -138,6 +138,7 @@ describe(`POST ${URL}`, () => { meta: { title: 'my-pattern-*', icon: 'index-pattern-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -187,6 +188,7 @@ describe(`POST ${URL}`, () => { meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present }); @@ -235,6 +237,7 @@ describe(`POST ${URL}`, () => { error: { type: 'conflict' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // successResults objects were not created because resolvable errors are present }); @@ -283,6 +286,7 @@ describe(`POST ${URL}`, () => { meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present }); @@ -336,6 +340,7 @@ describe(`POST ${URL}`, () => { meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( @@ -406,6 +411,7 @@ describe(`POST ${URL}`, () => { meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( @@ -470,6 +476,7 @@ describe(`POST ${URL}`, () => { meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( @@ -534,6 +541,7 @@ describe(`POST ${URL}`, () => { destinationId: obj2.id, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 7df3b62ab610c..6bb2660af06fa 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -113,7 +113,7 @@ describe(`POST ${URL}`, () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 0 }); + expect(result.body).toEqual({ success: true, successCount: 0, warnings: [] }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({ request: expect.anything(), @@ -153,6 +153,7 @@ describe(`POST ${URL}`, () => { success: true, successCount: 1, successResults: [{ type, id, meta }], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -190,6 +191,7 @@ describe(`POST ${URL}`, () => { success: true, successCount: 1, successResults: [{ type, id, meta }], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -228,6 +230,7 @@ describe(`POST ${URL}`, () => { success: true, successCount: 1, successResults: [{ type, id, meta, overwrite: true }], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -271,6 +274,7 @@ describe(`POST ${URL}`, () => { meta: { title: 'Look at my visualization', icon: 'visualization-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -319,6 +323,7 @@ describe(`POST ${URL}`, () => { meta: { title: 'Look at my visualization', icon: 'visualization-icon' }, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -383,6 +388,7 @@ describe(`POST ${URL}`, () => { destinationId: obj2.id, }, ], + warnings: [], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 9021d7f56c418..6db4cf4f781b4 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -43,6 +43,7 @@ import { SavedObjectsImporter, ISavedObjectsImporter } from './import'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; + /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to * use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index aa4ab623fe7a6..cbd8b415d9d31 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -9,6 +9,7 @@ import { SavedObjectsClient } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; +import { SavedObjectsImportHook } from './import/types'; export { SavedObjectsImportResponse, @@ -20,6 +21,9 @@ export { SavedObjectsImportUnknownError, SavedObjectsImportFailure, SavedObjectsImportRetry, + SavedObjectsImportActionRequiredWarning, + SavedObjectsImportSimpleWarning, + SavedObjectsImportWarning, } from './import/types'; import { SavedObject } from '../../types'; @@ -281,4 +285,46 @@ export interface SavedObjectsTypeManagementDefinition { * {@link Capabilities | uiCapabilities} to check if the user has permission to access the object. */ getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + /** + * An optional {@link SavedObjectsImportHook | import hook} to use when importing given type. + * + * Import hooks are executed during the savedObjects import process and allow to interact + * with the imported objects. See the {@link SavedObjectsImportHook | hook documentation} + * for more info. + * + * @example + * Registering a hook displaying a warning about a specific type of object + * ```ts + * // src/plugins/my_plugin/server/plugin.ts + * import { myType } from './saved_objects'; + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.savedObjects.registerType({ + * ...myType, + * management: { + * ...myType.management, + * onImport: (objects) => { + * if(someActionIsNeeded(objects)) { + * return { + * warnings: [ + * { + * type: 'action_required', + * message: 'Objects need to be manually enabled after import', + * actionPath: '/app/my-app/require-activation', + * }, + * ] + * } + * } + * return {}; + * } + * }, + * }); + * } + * } + * ``` + * + * @remark messages returned in the warnings are user facing and must be translated. + */ + onImport?: SavedObjectsImportHook; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a86e556136f78..3a8d7f4f0b0ff 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2473,6 +2473,15 @@ export interface SavedObjectsFindResult extends SavedObject { score: number; } +// @public +export interface SavedObjectsImportActionRequiredWarning { + actionPath: string; + buttonLabel?: string; + message: string; + // (undocumented) + type: 'action_required'; +} + // @public export interface SavedObjectsImportAmbiguousConflictError { // (undocumented) @@ -2542,6 +2551,14 @@ export interface SavedObjectsImportFailure { type: string; } +// @public +export type SavedObjectsImportHook = (objects: Array>) => SavedObjectsImportHookResult | Promise; + +// @public +export interface SavedObjectsImportHookResult { + warnings?: SavedObjectsImportWarning[]; +} + // @public export interface SavedObjectsImportMissingReferencesError { // (undocumented) @@ -2571,6 +2588,8 @@ export interface SavedObjectsImportResponse { successCount: number; // (undocumented) successResults?: SavedObjectsImportSuccess[]; + // (undocumented) + warnings: SavedObjectsImportWarning[]; } // @public @@ -2592,6 +2611,13 @@ export interface SavedObjectsImportRetry { type: string; } +// @public +export interface SavedObjectsImportSimpleWarning { + message: string; + // (undocumented) + type: 'simple'; +} + // @public export interface SavedObjectsImportSuccess { // @deprecated (undocumented) @@ -2625,6 +2651,9 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @public +export type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning; + // @public (undocumented) export interface SavedObjectsIncrementCounterField { fieldName: string; @@ -2790,6 +2819,7 @@ export interface SavedObjectsTypeManagementDefinition { getTitle?: (savedObject: SavedObject) => string; icon?: string; importableAndExportable?: boolean; + onImport?: SavedObjectsImportHook; } // @public diff --git a/src/plugins/saved_objects_management/public/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts index c5b3a9dc29950..a2f63571cbde4 100644 --- a/src/plugins/saved_objects_management/public/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -6,15 +6,9 @@ * Public License, v 1. */ -import { HttpStart, SavedObjectsImportFailure } from 'src/core/public'; +import { HttpStart, SavedObjectsImportResponse } from 'src/core/public'; import { ImportMode } from '../management_section/objects_table/components/import_mode_control'; -interface ImportResponse { - success: boolean; - successCount: number; - errors?: SavedObjectsImportFailure[]; -} - export async function importFile( http: HttpStart, file: File, @@ -23,7 +17,7 @@ export async function importFile( const formData = new FormData(); formData.append('file', file); const query = createNewCopies ? { createNewCopies } : { overwrite }; - return await http.post('/api/saved_objects/_import', { + return await http.post('/api/saved_objects/_import', { body: formData, headers: { // Important to be undefined, it forces proper headers to be set for FormData diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts index 5d467883448b5..a8f509ec4223d 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts @@ -11,14 +11,16 @@ import { SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnknownError, SavedObjectsImportMissingReferencesError, + SavedObjectsImportResponse, } from 'src/core/public'; import { processImportResponse } from './process_import_response'; describe('processImportResponse()', () => { test('works when no errors exist in the response', () => { - const response = { + const response: SavedObjectsImportResponse = { success: true, successCount: 0, + warnings: [], }; const result = processImportResponse(response); expect(result.status).toBe('success'); @@ -26,7 +28,7 @@ describe('processImportResponse()', () => { }); test('conflict errors get added to failedImports and result in idle status', () => { - const response = { + const response: SavedObjectsImportResponse = { success: false, successCount: 0, errors: [ @@ -39,6 +41,7 @@ describe('processImportResponse()', () => { meta: {}, }, ], + warnings: [], }; const result = processImportResponse(response); expect(result.failedImports).toMatchInlineSnapshot(` @@ -59,7 +62,7 @@ describe('processImportResponse()', () => { }); test('ambiguous conflict errors get added to failedImports and result in idle status', () => { - const response = { + const response: SavedObjectsImportResponse = { success: false, successCount: 0, errors: [ @@ -72,6 +75,7 @@ describe('processImportResponse()', () => { meta: {}, }, ], + warnings: [], }; const result = processImportResponse(response); expect(result.failedImports).toMatchInlineSnapshot(` @@ -92,7 +96,7 @@ describe('processImportResponse()', () => { }); test('unknown errors get added to failedImports and result in success status', () => { - const response = { + const response: SavedObjectsImportResponse = { success: false, successCount: 0, errors: [ @@ -105,6 +109,7 @@ describe('processImportResponse()', () => { meta: {}, }, ], + warnings: [], }; const result = processImportResponse(response); expect(result.failedImports).toMatchInlineSnapshot(` @@ -125,7 +130,7 @@ describe('processImportResponse()', () => { }); test('missing references get added to failedImports and result in idle status', () => { - const response = { + const response: SavedObjectsImportResponse = { success: false, successCount: 0, errors: [ @@ -144,6 +149,7 @@ describe('processImportResponse()', () => { meta: {}, }, ], + warnings: [], }; const result = processImportResponse(response); expect(result.failedImports).toMatchInlineSnapshot(` @@ -170,7 +176,7 @@ describe('processImportResponse()', () => { }); test('missing references get added to unmatchedReferences, but are not duplicated', () => { - const response = { + const response: SavedObjectsImportResponse = { success: false, successCount: 0, errors: [ @@ -188,6 +194,7 @@ describe('processImportResponse()', () => { meta: {}, }, ], + warnings: [], }; const result = processImportResponse(response); expect(result.unmatchedReferences).toEqual([ @@ -197,10 +204,11 @@ describe('processImportResponse()', () => { }); test('success results get added to successfulImports and result in success status', () => { - const response = { + const response: SavedObjectsImportResponse = { success: true, successCount: 1, successResults: [{ type: 'a', id: '1', meta: {} }], + warnings: [], }; const result = processImportResponse(response); expect(result.successfulImports).toMatchInlineSnapshot(` @@ -214,4 +222,22 @@ describe('processImportResponse()', () => { `); expect(result.status).toBe('success'); }); + + test('warnings from the response get returned', () => { + const response: SavedObjectsImportResponse = { + success: true, + successCount: 1, + successResults: [{ type: 'a', id: '1', meta: {} }], + warnings: [ + { + type: 'action_required', + message: 'foo', + actionPath: '/somewhere', + }, + ], + }; + const result = processImportResponse(response); + expect(result.status).toBe('success'); + expect(result.importWarnings).toEqual(response.warnings); + }); }); diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index eaee81b892995..f6f216602ab9e 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -15,6 +15,7 @@ import { SavedObjectsImportUnknownError, SavedObjectsImportFailure, SavedObjectsImportSuccess, + SavedObjectsImportWarning, } from 'src/core/public'; export interface FailedImport { @@ -41,6 +42,7 @@ export interface ProcessedImportResponse { importCount: number; conflictedSavedObjectsLinkedToSavedSearches: undefined; conflictedSearchDocs: undefined; + importWarnings: SavedObjectsImportWarning[]; } const isAnyConflict = ({ type }: FailedImport['error']) => @@ -87,5 +89,6 @@ export function processImportResponse( importCount: response.successCount, conflictedSavedObjectsLinkedToSavedSearches: undefined, conflictedSearchDocs: undefined, + importWarnings: response.warnings, }; } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index a48965cf7f41c..a68e8891b5ad1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -226,6 +226,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "createNewCopies": false, "overwrite": true, }, + "importWarnings": undefined, "indexPatterns": Array [ Object { "id": "1", @@ -668,7 +669,18 @@ exports[`Flyout should render import step 1`] = ` exports[`Flyout summary should display summary when import is complete 1`] = ` `; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index a93502c2605c0..0d8aa973bf5ea 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -18,7 +18,7 @@ import { import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; -import { coreMock } from '../../../../../../core/public/mocks'; +import { coreMock, httpServiceMock } from '../../../../../../core/public/mocks'; import { serviceRegistryMock } from '../../../services/service_registry.mock'; import { Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; @@ -47,6 +47,7 @@ describe('Flyout', () => { beforeEach(() => { const { http, overlays } = coreMock.createStart(); const search = dataPluginMock.createStartContract().search; + const basePath = httpServiceMock.createBasePath(); defaultProps = { close: jest.fn(), @@ -63,6 +64,7 @@ describe('Flyout', () => { allowedTypes: ['search', 'index-pattern', 'visualization'], serviceRegistry: serviceRegistryMock.create(), search, + basePath, }; }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index c8250863ab418..39a4529d1c231 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { OverlayStart, HttpStart } from 'src/core/public'; +import { OverlayStart, HttpStart, IBasePath } from 'src/core/public'; import { IndexPatternsContract, IIndexPattern, @@ -69,6 +69,7 @@ export interface FlyoutProps { indexPatterns: IndexPatternsContract; overlays: OverlayStart; http: HttpStart; + basePath: IBasePath; search: DataPublicPluginStart['search']; } @@ -81,6 +82,7 @@ export interface FlyoutState { failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; + importWarnings?: ProcessedImportResponse['importWarnings']; error?: string; file?: File; importCount: number; @@ -616,6 +618,7 @@ export class Flyout extends Component { successfulImports = [], isLegacyFile, importMode, + importWarnings, } = this.state; if (status === 'loading') { @@ -632,8 +635,15 @@ export class Flyout extends Component { ); } - if (isLegacyFile === false && status === 'success') { - return ; + if (!isLegacyFile && status === 'success') { + return ( + + ); } // Import summary for failed legacy import diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx index 7a7f7b2daa1a4..6cfd6f7fc57d0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx @@ -7,8 +7,9 @@ */ import React from 'react'; -import { ShallowWrapper } from 'enzyme'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; +import { ReactWrapper } from 'enzyme'; +import { mountWithI18nProvider } from '@kbn/test/jest'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; import { ImportSummary, ImportSummaryProps } from './import_summary'; import { FailedImport } from '../../../lib'; @@ -16,6 +17,20 @@ import { FailedImport } from '../../../lib'; import { findTestSubject } from '@elastic/eui/lib/test'; describe('ImportSummary', () => { + let basePath: ReturnType; + + const getProps = (parts: Partial): ImportSummaryProps => ({ + basePath, + failedImports: [], + successfulImports: [], + importWarnings: [], + ...parts, + }); + + beforeEach(() => { + basePath = httpServiceMock.createBasePath(); + }); + const errorUnsupportedType: FailedImport = { obj: { type: 'error-obj-type', id: 'error-obj-id', meta: { title: 'Error object' } }, error: { type: 'unsupported_type' }, @@ -28,19 +43,20 @@ describe('ImportSummary', () => { overwrite: true, }; - const findHeader = (wrapper: ShallowWrapper) => wrapper.find('h3'); - const findCountCreated = (wrapper: ShallowWrapper) => + const findHeader = (wrapper: ReactWrapper) => wrapper.find('h3'); + const findCountCreated = (wrapper: ReactWrapper) => wrapper.find('h4.savedObjectsManagementImportSummary__createdCount'); - const findCountOverwritten = (wrapper: ShallowWrapper) => + const findCountOverwritten = (wrapper: ReactWrapper) => wrapper.find('h4.savedObjectsManagementImportSummary__overwrittenCount'); - const findCountError = (wrapper: ShallowWrapper) => + const findCountError = (wrapper: ReactWrapper) => wrapper.find('h4.savedObjectsManagementImportSummary__errorCount'); - const findObjectRow = (wrapper: ShallowWrapper) => - wrapper.find('.savedObjectsManagementImportSummary__row'); + const findObjectRow = (wrapper: ReactWrapper) => + wrapper.find('.savedObjectsManagementImportSummary__row').hostNodes(); + const findWarnings = (wrapper: ReactWrapper) => wrapper.find('ImportWarning'); it('should render as expected with no results', async () => { - const props: ImportSummaryProps = { failedImports: [], successfulImports: [] }; - const wrapper = shallowWithI18nProvider(); + const props = getProps({ failedImports: [], successfulImports: [] }); + const wrapper = mountWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( expect.objectContaining({ values: { importCount: 0 } }) @@ -52,14 +68,14 @@ describe('ImportSummary', () => { }); it('should render as expected with a newly created object', async () => { - const props: ImportSummaryProps = { + const props = getProps({ failedImports: [], successfulImports: [successNew], - }; - const wrapper = shallowWithI18nProvider(); + }); + const wrapper = mountWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular + expect.objectContaining({ values: { importCount: 1 } }) ); const countCreated = findCountCreated(wrapper); expect(countCreated).toHaveLength(1); @@ -68,18 +84,19 @@ describe('ImportSummary', () => { ); expect(findCountOverwritten(wrapper)).toHaveLength(0); expect(findCountError(wrapper)).toHaveLength(0); + expect(findObjectRow(wrapper)).toHaveLength(1); }); it('should render as expected with an overwritten object', async () => { - const props: ImportSummaryProps = { + const props = getProps({ failedImports: [], successfulImports: [successOverwritten], - }; - const wrapper = shallowWithI18nProvider(); + }); + const wrapper = mountWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular + expect.objectContaining({ values: { importCount: 1 } }) ); expect(findCountCreated(wrapper)).toHaveLength(0); const countOverwritten = findCountOverwritten(wrapper); @@ -92,14 +109,14 @@ describe('ImportSummary', () => { }); it('should render as expected with an error object', async () => { - const props: ImportSummaryProps = { + const props = getProps({ failedImports: [errorUnsupportedType], successfulImports: [], - }; - const wrapper = shallowWithI18nProvider(); + }); + const wrapper = mountWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular + expect.objectContaining({ values: { importCount: 1 } }) ); expect(findCountCreated(wrapper)).toHaveLength(0); expect(findCountOverwritten(wrapper)).toHaveLength(0); @@ -112,11 +129,11 @@ describe('ImportSummary', () => { }); it('should render as expected with mixed objects', async () => { - const props: ImportSummaryProps = { + const props = getProps({ failedImports: [errorUnsupportedType], successfulImports: [successNew, successOverwritten], - }; - const wrapper = shallowWithI18nProvider(); + }); + const wrapper = mountWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( expect.objectContaining({ values: { importCount: 3 } }) @@ -138,4 +155,24 @@ describe('ImportSummary', () => { ); expect(findObjectRow(wrapper)).toHaveLength(3); }); + + it('should render warnings when present', async () => { + const props = getProps({ + successfulImports: [successNew], + importWarnings: [ + { + type: 'simple', + message: 'foo', + }, + { + type: 'action_required', + message: 'bar', + actionPath: '/app/lost', + }, + ], + }); + const wrapper = mountWithI18nProvider(); + + expect(findWarnings(wrapper)).toHaveLength(2); + }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx index f562c3ca3f922..201bcc6f89cf5 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx @@ -8,11 +8,13 @@ import './import_summary.scss'; import _ from 'lodash'; -import React, { Fragment } from 'react'; +import React, { Fragment, FC, useMemo } from 'react'; import { EuiText, EuiFlexGroup, EuiFlexItem, + EuiCallOut, + EuiButton, EuiToolTip, EuiIcon, EuiIconTip, @@ -22,15 +24,20 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsImportSuccess } from 'kibana/public'; -import { FailedImport } from '../../..'; -import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import type { + SavedObjectsImportSuccess, + SavedObjectsImportWarning, + IBasePath, +} from 'kibana/public'; +import { getDefaultTitle, getSavedObjectLabel, FailedImport } from '../../../lib'; const DEFAULT_ICON = 'apps'; export interface ImportSummaryProps { failedImports: FailedImport[]; successfulImports: SavedObjectsImportSuccess[]; + importWarnings: SavedObjectsImportWarning[]; + basePath: IBasePath; } interface ImportItem { @@ -72,7 +79,7 @@ const mapImportSuccess = (obj: SavedObjectsImportSuccess): ImportItem => { return { type, id, title, icon, outcome }; }; -const getCountIndicators = (importItems: ImportItem[]) => { +const CountIndicators: FC<{ importItems: ImportItem[] }> = ({ importItems }) => { if (!importItems.length) { return null; } @@ -130,7 +137,8 @@ const getCountIndicators = (importItems: ImportItem[]) => { ); }; -const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) => { +const StatusIndicator: FC<{ item: ImportItem }> = ({ item }) => { + const { outcome, errorMessage = 'Error' } = item; switch (outcome) { case 'created': return ( @@ -165,13 +173,85 @@ const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) => } }; -export const ImportSummary = ({ failedImports, successfulImports }: ImportSummaryProps) => { - const importItems: ImportItem[] = _.sortBy( - [ - ...failedImports.map((x) => mapFailedImport(x)), - ...successfulImports.map((x) => mapImportSuccess(x)), - ], - ['type', 'title'] +const ImportWarnings: FC<{ warnings: SavedObjectsImportWarning[]; basePath: IBasePath }> = ({ + warnings, + basePath, +}) => { + if (!warnings.length) { + return null; + } + + return ( + <> + + {warnings.map((warning, index) => ( + + + {index < warnings.length - 1 && } + + ))} + + ); +}; + +const ImportWarning: FC<{ warning: SavedObjectsImportWarning; basePath: IBasePath }> = ({ + warning, + basePath, +}) => { + const warningContent = useMemo(() => { + if (warning.type === 'action_required') { + return ( + + + + {warning.buttonLabel || ( + + )} + + + + ); + } + return null; + }, [warning, basePath]); + + return ( + + {warningContent} + + ); +}; + +export const ImportSummary: FC = ({ + failedImports, + successfulImports, + importWarnings, + basePath, +}) => { + const importItems: ImportItem[] = useMemo( + () => + _.sortBy( + [ + ...failedImports.map((x) => mapFailedImport(x)), + ...successfulImports.map((x) => mapImportSuccess(x)), + ], + ['type', 'title'] + ), + [successfulImports, failedImports] ); return ( @@ -183,22 +263,16 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar } >

- {importItems.length === 1 ? ( - - ) : ( - - )} +

- - {getCountIndicators(importItems)} + + + {importItems.map((item, index) => { const { type, title, icon } = item; @@ -223,7 +297,9 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar

yOoL6TI8b>-HBJnuK`;amZb3}! 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

-
{getStatusIndicator(item)}
+
+ +
); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 69ae11038122d..bd70d34ae6854 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -501,6 +501,7 @@ export class SavedObjectsTable extends Component ); diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index 4dcaa4bd99932..573ad60482e27 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -74,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { createConflictError(visualization), createConflictError(dashboard), ], + warnings: [], }); }); }); @@ -93,6 +94,7 @@ export default function ({ getService }: FtrProviderContext) { { ...visualization, overwrite: true }, { ...dashboard, overwrite: true }, ], + warnings: [], }); }); }); @@ -119,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) { error: { type: 'unsupported_type' }, }, ], + warnings: [], }); }); }); @@ -157,6 +160,7 @@ export default function ({ getService }: FtrProviderContext) { type: 'dashboard', }, ], + warnings: [], }); }); @@ -227,6 +231,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, ], + warnings: [], }); }); }); diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 5f3929f26aba6..3686c46b229b1 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -46,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ success: true, successCount: 0, + warnings: [], }); }); }); @@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { { ...visualization, overwrite: true }, { ...dashboard, overwrite: true }, ], + warnings: [], }); }); }); @@ -125,6 +127,7 @@ export default function ({ getService }: FtrProviderContext) { error: { type: 'unsupported_type' }, }, ], + warnings: [], }); }); }); @@ -198,6 +201,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, ], + warnings: [], }); }); }); @@ -215,7 +219,7 @@ export default function ({ getService }: FtrProviderContext) { .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { - expect(resp.body).to.eql({ success: true, successCount: 0 }); + expect(resp.body).to.eql({ success: true, successCount: 0, warnings: [] }); }); }); @@ -253,6 +257,7 @@ export default function ({ getService }: FtrProviderContext) { { ...visualization, overwrite: true }, { ...dashboard, overwrite: true }, ], + warnings: [], }); }); }); @@ -277,6 +282,7 @@ export default function ({ getService }: FtrProviderContext) { success: true, successCount: 1, successResults: [{ ...visualization, overwrite: true }], + warnings: [], }); }); }); @@ -328,6 +334,7 @@ export default function ({ getService }: FtrProviderContext) { meta: { title: 'My favorite vis', icon: 'visualizeApp' }, }, ], + warnings: [], }); }); await supertest diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index e29a9abadd881..1cdf76ad58ef0 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -283,6 +283,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv await testSubjects.click('confirmModalConfirmButton'); await this.waitTableIsLoaded(); } + + async getImportWarnings() { + const elements = await testSubjects.findAll('importSavedObjectsWarning'); + return Promise.all( + elements.map(async (element) => { + const message = await element + .findByClassName('euiCallOutHeader__title') + .then((titleEl) => titleEl.getVisibleText()); + const buttons = await element.findAllByClassName('euiButton'); + return { + message, + type: buttons.length ? 'action_required' : 'simple', + }; + }) + ); + } } return new SavedObjectsPage(); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index 9822ba3bee8da..2842a18c9445a 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -29,6 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/doc_views'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), + require.resolve('./test_suites/saved_objects_management'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/saved_object_hooks/kibana.json b/test/plugin_functional/plugins/saved_object_hooks/kibana.json new file mode 100644 index 0000000000000..1580e1862fac1 --- /dev/null +++ b/test/plugin_functional/plugins/saved_object_hooks/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "savedObjectHooks", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["saved_object_hooks"], + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/saved_object_hooks/package.json b/test/plugin_functional/plugins/saved_object_hooks/package.json new file mode 100644 index 0000000000000..9e09e5fc94be4 --- /dev/null +++ b/test/plugin_functional/plugins/saved_object_hooks/package.json @@ -0,0 +1,14 @@ +{ + "name": "saved_object_hooks", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/saved_object_hooks", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/saved_object_hooks/server/index.ts b/test/plugin_functional/plugins/saved_object_hooks/server/index.ts new file mode 100644 index 0000000000000..28aaa75961ddc --- /dev/null +++ b/test/plugin_functional/plugins/saved_object_hooks/server/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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectHooksPlugin } from './plugin'; + +export const plugin = () => new SavedObjectHooksPlugin(); diff --git a/test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts b/test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts new file mode 100644 index 0000000000000..823d9a90f29e2 --- /dev/null +++ b/test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; + +export class SavedObjectHooksPlugin implements Plugin { + public setup({ savedObjects }: CoreSetup, deps: {}) { + savedObjects.registerType({ + name: 'test_import_warning_1', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj) => obj.attributes.title, + onImport: (objects) => { + return { + warnings: [{ type: 'simple', message: 'warning for test_import_warning_1' }], + }; + }, + }, + }); + + savedObjects.registerType({ + name: 'test_import_warning_2', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj) => obj.attributes.title, + onImport: (objects) => { + return { + warnings: [ + { + type: 'action_required', + message: 'warning for test_import_warning_2', + actionPath: '/some/url', + }, + ], + }; + }, + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/saved_object_hooks/tsconfig.json b/test/plugin_functional/plugins/saved_object_hooks/tsconfig.json new file mode 100644 index 0000000000000..3d9d8ca9451d4 --- /dev/null +++ b/test/plugin_functional/plugins/saved_object_hooks/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/exports/_import_both_types.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_both_types.ndjson new file mode 100644 index 0000000000000..d72511238e38f --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_both_types.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1} +{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_1.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_1.ndjson new file mode 100644 index 0000000000000..f24f73880190a --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_1.ndjson @@ -0,0 +1 @@ +{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_2.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_2.ndjson new file mode 100644 index 0000000000000..15efd8a6ce03d --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_type_2.ndjson @@ -0,0 +1 @@ +{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts new file mode 100644 index 0000000000000..71663b19b35cb --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import path from 'path'; +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + + describe('import warnings', () => { + beforeEach(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + it('should display simple warnings', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_type_1.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + message: 'warning for test_import_warning_1', + type: 'simple', + }, + ]); + }); + + it('should display action warnings', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_type_2.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + type: 'action_required', + message: 'warning for test_import_warning_2', + }, + ]); + }); + + it('should display warnings coming from multiple types', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_both_types.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + message: 'warning for test_import_warning_1', + type: 'simple', + }, + { + type: 'action_required', + message: 'warning for test_import_warning_2', + }, + ]); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/index.ts b/test/plugin_functional/test_suites/saved_objects_management/index.ts new file mode 100644 index 0000000000000..ad89a6605bbc5 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ loadTestFile }: PluginFunctionalProviderContext) { + describe('Saved Objects Management', function () { + loadTestFile(require.resolve('./import_warnings')); + }); +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index 5ede5f8a38797..2c6ec23290bb4 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -177,6 +177,7 @@ describe('CopyToSpaceFlyout', () => { 'space-1': { success: true, successCount: 3, + warnings: [], }, 'space-2': { success: false, @@ -195,6 +196,7 @@ describe('CopyToSpaceFlyout', () => { meta: {}, }, ], + warnings: [], }, }); @@ -259,10 +261,12 @@ describe('CopyToSpaceFlyout', () => { 'space-1': { success: true, successCount: 3, + warnings: [], }, 'space-2': { success: true, successCount: 3, + warnings: [], }, }); @@ -319,6 +323,7 @@ describe('CopyToSpaceFlyout', () => { 'space-1': { success: true, successCount: 5, + warnings: [], }, 'space-2': { success: false, @@ -359,6 +364,7 @@ describe('CopyToSpaceFlyout', () => { meta: {}, }, ], + warnings: [], }, }); @@ -366,6 +372,7 @@ describe('CopyToSpaceFlyout', () => { 'space-2': { success: true, successCount: 2, + warnings: [], }, }); @@ -490,6 +497,7 @@ describe('CopyToSpaceFlyout', () => { }, ], successResults: [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id, meta: {} }], + warnings: [], }, }); @@ -571,6 +579,7 @@ describe('CopyToSpaceFlyout', () => { 'space-1': { success: true, successCount: 3, + warnings: [], }, 'space-2': { success: false, @@ -583,6 +592,7 @@ describe('CopyToSpaceFlyout', () => { meta: {}, }, ], + warnings: [], }, }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index db731713811b4..8450fdf6b4641 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -109,6 +109,7 @@ describe('copySavedObjectsToSpaces', () => { success: true, successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }; return Promise.resolve(response); @@ -201,6 +202,7 @@ describe('copySavedObjectsToSpaces', () => { success: true, successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }); }, }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index a558044d413d1..0f5de232177fd 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -109,6 +109,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { success: true, successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }; return response; @@ -209,6 +210,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { success: true, successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }); }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 65298463c9808..07befe8a26b2f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3184,8 +3184,6 @@ "savedObjectsManagement.importSummary.createdOutcomeLabel": "作成済み", "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount}件のエラー", "savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}", - "savedObjectsManagement.importSummary.headerLabelPlural": "{importCount}個のオブジェクトがインポートされました", - "savedObjectsManagement.importSummary.headerLabelSingular": "1個のオブジェクトがインポートされました", "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount}件上書きされました", "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7befbcf34e4d8..87af04f7dec87 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3188,8 +3188,6 @@ "savedObjectsManagement.importSummary.createdOutcomeLabel": "已创建", "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount} 个错误", "savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}", - "savedObjectsManagement.importSummary.headerLabelPlural": "{importCount} 个对象已导入", - "savedObjectsManagement.importSummary.headerLabelSingular": "1 个对象已导入", "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount} 个已覆盖", "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "已覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", From f0f192c654010e2038a156add1fd777ec5f8a370 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 20 Jan 2021 11:25:48 -0700 Subject: [PATCH 15/28] [Maps] fix Maps should display better error message instead of EsError when there is no data for tracks data source (#88847) --- .../es_geo_line_source/es_geo_line_source.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 3693f55586b34..9c851dcedb3fa 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -9,7 +9,12 @@ import React from 'react'; import { GeoJsonProperties } from 'geojson'; import { i18n } from '@kbn/i18n'; -import { FIELD_ORIGIN, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { + EMPTY_FEATURE_COLLECTION, + FIELD_ORIGIN, + SOURCE_TYPES, + VECTOR_SHAPE_TYPE, +} from '../../../../common/constants'; import { getField, addFieldToDSL } from '../../../../common/elasticsearch_util'; import { ESGeoLineSourceDescriptor, @@ -216,6 +221,18 @@ export class ESGeoLineSource extends AbstractESAggSource { ); const totalEntities = _.get(entityResp, 'aggregations.totalEntities.value', 0); const areEntitiesTrimmed = entityBuckets.length >= MAX_TRACKS; + if (totalEntities === 0) { + return { + data: EMPTY_FEATURE_COLLECTION, + meta: { + areResultsTrimmed: false, + areEntitiesTrimmed: false, + entityCount: 0, + numTrimmedTracks: 0, + totalEntities: 0, + } as ESGeoLineSourceResponseMeta, + }; + } // // Fetch tracks From c9002a25c50b4074aac4a40f67882bf0d88aaba5 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 20 Jan 2021 13:43:53 -0500 Subject: [PATCH 16/28] [Monitoring] Convert Elasticsearch-related server files that read from _source to typescript (#88212) * A good chunk of server-side ES changes * CCR files * More areas where we just pass down the source to the client * Some more * Fix tests * Fix tests and types --- x-pack/plugins/monitoring/common/types/es.ts | 206 +++++++++++++++++- .../server/lib/apm/_get_time_of_last_event.ts | 5 +- .../monitoring/server/lib/apm/get_apm_info.ts | 3 +- .../monitoring/server/lib/apm/get_apms.ts | 3 +- .../server/lib/beats/get_beat_summary.ts | 3 +- .../monitoring/server/lib/beats/get_beats.ts | 3 +- ...clusters.js => flag_supported_clusters.ts} | 39 ++-- ...ster_license.js => get_cluster_license.ts} | 12 +- ...luster_status.js => get_cluster_status.ts} | 30 +-- ...lusters_state.js => get_clusters_state.ts} | 20 +- ...lusters_stats.js => get_clusters_stats.ts} | 21 +- .../lib/elasticsearch/{ccr.js => ccr.ts} | 15 +- ..._last_recovery.js => get_last_recovery.ts} | 17 +- .../{get_ml_jobs.js => get_ml_jobs.ts} | 30 ++- ..._index_summary.js => get_index_summary.ts} | 36 +-- .../{get_indices.js => get_indices.ts} | 69 ++++-- ...et_node_summary.js => get_node_summary.ts} | 78 ++++--- .../get_nodes/{get_nodes.js => get_nodes.ts} | 18 +- ...{handle_response.js => handle_response.ts} | 43 ++-- .../nodes/get_nodes/map_nodes_info.js | 46 ---- .../nodes/get_nodes/map_nodes_info.ts | 57 +++++ ..._allocation.js => get_shard_allocation.ts} | 24 +- .../server/lib/kibana/get_kibana_info.ts | 5 +- .../server/lib/logstash/get_node_info.ts | 3 +- .../logstash/get_pipeline_state_document.ts | 3 +- .../api/v1/elasticsearch/{ccr.js => ccr.ts} | 109 ++++++--- .../{ccr_shard.js => ccr_shard.ts} | 29 +-- x-pack/plugins/monitoring/server/types.ts | 24 -- 28 files changed, 655 insertions(+), 296 deletions(-) rename x-pack/plugins/monitoring/server/lib/cluster/{flag_supported_clusters.js => flag_supported_clusters.ts} (79%) rename x-pack/plugins/monitoring/server/lib/cluster/{get_cluster_license.js => get_cluster_license.ts} (70%) rename x-pack/plugins/monitoring/server/lib/cluster/{get_cluster_status.js => get_cluster_status.ts} (53%) rename x-pack/plugins/monitoring/server/lib/cluster/{get_clusters_state.js => get_clusters_state.ts} (82%) rename x-pack/plugins/monitoring/server/lib/cluster/{get_clusters_stats.js => get_clusters_stats.ts} (83%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/{ccr.js => ccr.ts} (72%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/{get_last_recovery.js => get_last_recovery.ts} (81%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/{get_ml_jobs.js => get_ml_jobs.ts} (79%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/indices/{get_index_summary.js => get_index_summary.ts} (73%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/indices/{get_indices.js => get_indices.ts} (72%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/{get_node_summary.js => get_node_summary.ts} (59%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/{get_nodes.js => get_nodes.ts} (87%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/{handle_response.js => handle_response.ts} (57%) delete mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js create mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts rename x-pack/plugins/monitoring/server/lib/elasticsearch/shards/{get_shard_allocation.js => get_shard_allocation.ts} (75%) rename x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/{ccr.js => ccr.ts} (72%) rename x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/{ccr_shard.js => ccr_shard.ts} (82%) diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index 725ff214ae795..728cd3d73a34c 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -4,6 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +export interface ElasticsearchResponse { + hits?: { + hits: ElasticsearchResponseHit[]; + total: { + value: number; + }; + }; + aggregations?: any; +} + +export interface ElasticsearchResponseHit { + _index: string; + _source: ElasticsearchSource; + inner_hits?: { + [field: string]: { + hits?: { + hits: ElasticsearchResponseHit[]; + total: { + value: number; + }; + }; + }; + }; +} + export interface ElasticsearchSourceKibanaStats { timestamp?: string; kibana?: { @@ -34,9 +59,94 @@ export interface ElasticsearchSourceLogstashPipelineVertex { }; } -export interface ElasticsearchSource { +export interface ElasticsearchNodeStats { + indices?: { + docs?: { + count?: number; + }; + store?: { + size_in_bytes?: number; + size?: { + bytes?: number; + }; + }; + }; + fs?: { + total?: { + available_in_bytes?: number; + total_in_bytes?: number; + }; + summary?: { + available?: { + bytes?: number; + }; + total?: { + bytes?: number; + }; + }; + }; + jvm?: { + mem?: { + heap_used_percent?: number; + heap?: { + used?: { + pct?: number; + }; + }; + }; + }; +} + +export interface ElasticsearchLegacySource { timestamp: string; + cluster_uuid: string; + cluster_stats?: { + nodes?: { + count?: { + total?: number; + }; + jvm?: { + max_uptime_in_millis?: number; + mem?: { + heap_used_in_bytes?: number; + heap_max_in_bytes?: number; + }; + }; + versions?: string[]; + }; + indices?: { + count?: number; + docs?: { + count?: number; + }; + shards?: { + total?: number; + }; + store?: { + size_in_bytes?: number; + }; + }; + }; + cluster_state?: { + status?: string; + nodes?: { + [nodeUuid: string]: {}; + }; + master_node?: boolean; + }; + source_node?: { + id?: string; + uuid?: string; + attributes?: {}; + transport_address?: string; + name?: string; + type?: string; + }; kibana_stats?: ElasticsearchSourceKibanaStats; + license?: { + status?: string; + type?: string; + }; logstash_state?: { pipeline?: { representation?: { @@ -108,4 +218,98 @@ export interface ElasticsearchSource { }; }; }; + stack_stats?: { + xpack?: { + ccr?: { + enabled?: boolean; + available?: boolean; + }; + }; + }; + job_stats?: { + job_id?: number; + state?: string; + data_counts?: { + processed_record_count?: number; + }; + model_size_stats?: { + model_bytes?: number; + }; + forecasts_stats?: { + total?: number; + }; + node?: { + id?: number; + name?: string; + }; + }; + index_stats?: { + index?: string; + primaries?: { + docs?: { + count?: number; + }; + store?: { + size_in_bytes?: number; + }; + indexing?: { + index_total?: number; + }; + }; + total?: { + store?: { + size_in_bytes?: number; + }; + search?: { + query_total?: number; + }; + }; + }; + node_stats?: ElasticsearchNodeStats; + service?: { + address?: string; + }; + shard?: { + index?: string; + shard?: string; + primary?: boolean; + relocating_node?: string; + node?: string; + }; + ccr_stats?: { + leader_index?: string; + follower_index?: string; + shard_id?: number; + read_exceptions?: Array<{ + exception?: { + type?: string; + }; + }>; + time_since_last_read_millis?: number; + }; + index_recovery?: { + shards?: ElasticsearchIndexRecoveryShard[]; + }; +} + +export interface ElasticsearchIndexRecoveryShard { + start_time_in_millis: number; + stop_time_in_millis: number; +} + +export interface ElasticsearchMetricbeatNode { + stats?: ElasticsearchNodeStats; +} + +export interface ElasticsearchMetricbeatSource { + elasticsearch?: { + node?: ElasticsearchLegacySource['source_node'] & ElasticsearchMetricbeatNode; + }; +} + +export type ElasticsearchSource = ElasticsearchLegacySource & ElasticsearchMetricbeatSource; + +export interface ElasticsearchModifiedSource extends ElasticsearchSource { + ccs?: string; + isSupported?: boolean; } diff --git a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts index fc103959381bc..68f16cf23b474 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts @@ -8,7 +8,8 @@ import { createApmQuery } from './create_apm_query'; // @ts-ignore import { ApmClusterMetric } from '../metrics'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; export async function getTimeOfLastEvent({ req, @@ -58,5 +59,5 @@ export async function getTimeOfLastEvent({ }; const response = await callWithRequest(req, 'search', params); - return response.hits?.hits.length ? response.hits?.hits[0]._source.timestamp : undefined; + return response.hits?.hits.length ? response.hits?.hits[0]?._source.timestamp : undefined; } diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts index 7d471d528595e..7bc36d559ac34 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts @@ -14,7 +14,8 @@ import { getDiffCalculation } from '../beats/_beats_stats'; // @ts-ignore import { ApmMetric } from '../metrics'; import { getTimeOfLastEvent } from './_get_time_of_last_event'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; export function handleResponse(response: ElasticsearchResponse, apmUuid: string) { if (!response.hits || response.hits.hits.length === 0) { diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts index 7677677ea5e75..4dbd32c889760 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts @@ -14,7 +14,8 @@ import { createApmQuery } from './create_apm_query'; import { calculateRate } from '../calculate_rate'; // @ts-ignore import { getDiffCalculation } from './_apm_stats'; -import { LegacyRequest, ElasticsearchResponse, ElasticsearchResponseHit } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../../common/types/es'; export function handleResponse(response: ElasticsearchResponse, start: number, end: number) { const initial = { ids: new Set(), beats: [] }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts index 80b5efda4047a..0bfc4b85c9661 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts @@ -5,7 +5,8 @@ */ import { upperFirst } from 'lodash'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; // @ts-ignore import { checkParam } from '../error_missing_required'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts index aa5ef81a8de33..cd474f77d42c2 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts @@ -14,7 +14,8 @@ import { createBeatsQuery } from './create_beats_query'; import { calculateRate } from '../calculate_rate'; // @ts-ignore import { getDiffCalculation } from './_beats_stats'; -import { ElasticsearchResponse, LegacyRequest } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; interface Beat { uuid: string | undefined; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts similarity index 79% rename from x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js rename to x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts index a1674b2f5eb36..248d1604ee20b 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from '@elastic/safer-lodash-set'; -import { get, find } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; +import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; async function findSupportedBasicLicenseCluster( - req, - clusters, - kbnIndexPattern, - kibanaUuid, - serverLog + req: LegacyRequest, + clusters: ElasticsearchModifiedSource[], + kbnIndexPattern: string, + kibanaUuid: string, + serverLog: (message: string) => void ) { checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/findSupportedBasicLicenseCluster'); @@ -25,7 +26,7 @@ async function findSupportedBasicLicenseCluster( const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const gte = req.payload.timeRange.min; const lte = req.payload.timeRange.max; - const kibanaDataResult = await callWithRequest(req, 'search', { + const kibanaDataResult: ElasticsearchResponse = (await callWithRequest(req, 'search', { index: kbnIndexPattern, size: 1, ignoreUnavailable: true, @@ -42,11 +43,13 @@ async function findSupportedBasicLicenseCluster( }, }, }, - }); - const supportedClusterUuid = get(kibanaDataResult, 'hits.hits[0]._source.cluster_uuid'); - const supportedCluster = find(clusters, { cluster_uuid: supportedClusterUuid }); - // only this basic cluster is supported - set(supportedCluster, 'isSupported', true); + })) as ElasticsearchResponse; + const supportedClusterUuid = kibanaDataResult.hits?.hits[0]?._source.cluster_uuid ?? undefined; + for (const cluster of clusters) { + if (cluster.cluster_uuid === supportedClusterUuid) { + cluster.isSupported = true; + } + } serverLog( `Found basic license admin cluster UUID for Monitoring UI support: ${supportedClusterUuid}.` @@ -69,12 +72,12 @@ async function findSupportedBasicLicenseCluster( * Non-Basic license clusters and any cluster in a single-cluster environment * are also flagged as supported in this method. */ -export function flagSupportedClusters(req, kbnIndexPattern) { +export function flagSupportedClusters(req: LegacyRequest, kbnIndexPattern: string) { checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/flagSupportedClusters'); const config = req.server.config(); - const serverLog = (msg) => req.getLogger('supported-clusters').debug(msg); - const flagAllSupported = (clusters) => { + const serverLog = (message: string) => req.getLogger('supported-clusters').debug(message); + const flagAllSupported = (clusters: ElasticsearchModifiedSource[]) => { clusters.forEach((cluster) => { if (cluster.license) { cluster.isSupported = true; @@ -83,7 +86,7 @@ export function flagSupportedClusters(req, kbnIndexPattern) { return clusters; }; - return async function (clusters) { + return async function (clusters: ElasticsearchModifiedSource[]) { // Standalone clusters are automatically supported in the UI so ignore those for // our calculations here let linkedClusterCount = 0; @@ -110,7 +113,7 @@ export function flagSupportedClusters(req, kbnIndexPattern) { // if all linked are basic licenses if (linkedClusterCount === basicLicenseCount) { - const kibanaUuid = config.get('server.uuid'); + const kibanaUuid = config.get('server.uuid') as string; return await findSupportedBasicLicenseCluster( req, clusters, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts similarity index 70% rename from x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts index bd84fbb66f962..9f3106f7c04a3 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { createQuery } from '../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../metrics'; +import { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; -export function getClusterLicense(req, esIndexPattern, clusterUuid) { +export function getClusterLicense(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { checkParam(esIndexPattern, 'esIndexPattern in getClusterLicense'); const params = { @@ -28,7 +32,7 @@ export function getClusterLicense(req, esIndexPattern, clusterUuid) { }; const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((response) => { - return get(response, 'hits.hits[0]._source.license', {}); + return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => { + return response.hits?.hits[0]?._source.license; }); } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.ts similarity index 53% rename from x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.ts index cef06bb473c3f..3184893d6c637 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.ts @@ -3,20 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { get } from 'lodash'; +import { ElasticsearchSource } from '../../../common/types/es'; /* * @param cluster {Object} clusterStats from getClusterStatus * @param unassignedShards {Object} shardStats from getShardStats * @return top-level cluster summary data */ -export function getClusterStatus(cluster, shardStats) { - const clusterStats = get(cluster, 'cluster_stats', {}); - const clusterNodes = get(clusterStats, 'nodes', {}); - const clusterIndices = get(clusterStats, 'indices', {}); +export function getClusterStatus(cluster: ElasticsearchSource, shardStats: unknown) { + const clusterStats = cluster.cluster_stats ?? {}; + const clusterNodes = clusterStats.nodes ?? {}; + const clusterIndices = clusterStats.indices ?? {}; - const clusterTotalShards = get(clusterIndices, 'shards.total', 0); + const clusterTotalShards = clusterIndices.shards?.total ?? 0; let unassignedShardsTotal = 0; const unassignedShards = get(shardStats, 'indicesTotals.unassigned'); if (unassignedShards !== undefined) { @@ -26,17 +26,17 @@ export function getClusterStatus(cluster, shardStats) { const totalShards = clusterTotalShards + unassignedShardsTotal; return { - status: get(cluster, 'cluster_state.status', 'unknown'), + status: cluster.cluster_state?.status ?? 'unknown', // index-based stats - indicesCount: get(clusterIndices, 'count', 0), - documentCount: get(clusterIndices, 'docs.count', 0), - dataSize: get(clusterIndices, 'store.size_in_bytes', 0), + indicesCount: clusterIndices.count ?? 0, + documentCount: clusterIndices.docs?.count ?? 0, + dataSize: clusterIndices.store?.size_in_bytes ?? 0, // node-based stats - nodesCount: get(clusterNodes, 'count.total', 0), - upTime: get(clusterNodes, 'jvm.max_uptime_in_millis', 0), - version: get(clusterNodes, 'versions', null), - memUsed: get(clusterNodes, 'jvm.mem.heap_used_in_bytes', 0), - memMax: get(clusterNodes, 'jvm.mem.heap_max_in_bytes', 0), + nodesCount: clusterNodes.count?.total ?? 0, + upTime: clusterNodes.jvm?.max_uptime_in_millis ?? 0, + version: clusterNodes.versions ?? null, + memUsed: clusterNodes.jvm?.mem?.heap_used_in_bytes ?? 0, + memMax: clusterNodes.jvm?.mem?.heap_max_in_bytes ?? 0, unassignedShards: unassignedShardsTotal, totalShards, }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts similarity index 82% rename from x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts index fa5526728086e..c752f218f9626 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, find } from 'lodash'; +import { find } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; /** * Augment the {@clusters} with their cluster state's from the {@code response}. @@ -15,11 +18,14 @@ import { checkParam } from '../error_missing_required'; * @param {Array} clusters Array of clusters to be augmented * @return {Array} Always {@code clusters}. */ -export function handleResponse(response, clusters) { - const hits = get(response, 'hits.hits', []); +export function handleResponse( + response: ElasticsearchResponse, + clusters: ElasticsearchModifiedSource[] +) { + const hits = response.hits?.hits ?? []; hits.forEach((hit) => { - const currentCluster = get(hit, '_source', {}); + const currentCluster = hit._source; if (currentCluster) { const cluster = find(clusters, { cluster_uuid: currentCluster.cluster_uuid }); @@ -39,7 +45,11 @@ export function handleResponse(response, clusters) { * * If there is no cluster state available for any cluster, then it will be returned without any cluster state information. */ -export function getClustersState(req, esIndexPattern, clusters) { +export function getClustersState( + req: LegacyRequest, + esIndexPattern: string, + clusters: ElasticsearchModifiedSource[] +) { checkParam(esIndexPattern, 'esIndexPattern in cluster/getClustersHealth'); const clusterUuids = clusters diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts similarity index 83% rename from x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index 8ddd33837f56e..609c8fb2089de 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -4,12 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { createQuery } from '../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../metrics'; +// @ts-ignore import { parseCrossClusterPrefix } from '../ccs_utils'; import { getClustersState } from './get_clusters_state'; +import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; /** * This will fetch the cluster stats and cluster state as a single object per cluster. @@ -19,10 +24,10 @@ import { getClustersState } from './get_clusters_state'; * @param {String} clusterUuid (optional) If not undefined, getClusters will filter for a single cluster * @return {Promise} A promise containing an array of clusters. */ -export function getClustersStats(req, esIndexPattern, clusterUuid) { +export function getClustersStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { return ( fetchClusterStats(req, esIndexPattern, clusterUuid) - .then((response) => handleClusterStats(response, req.server)) + .then((response) => handleClusterStats(response)) // augment older documents (e.g., from 2.x - 5.4) with their cluster_state .then((clusters) => getClustersState(req, esIndexPattern, clusters)) ); @@ -36,7 +41,7 @@ export function getClustersStats(req, esIndexPattern, clusterUuid) { * @param {String} clusterUuid (optional) - if not undefined, getClusters filters for a single clusterUuid * @return {Promise} Object representing each cluster. */ -function fetchClusterStats(req, esIndexPattern, clusterUuid) { +function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { checkParam(esIndexPattern, 'esIndexPattern in getClusters'); const config = req.server.config(); @@ -81,15 +86,15 @@ function fetchClusterStats(req, esIndexPattern, clusterUuid) { * @param {Object} response The response from Elasticsearch. * @return {Array} Objects representing each cluster. */ -export function handleClusterStats(response) { - const hits = get(response, 'hits.hits', []); +export function handleClusterStats(response: ElasticsearchResponse) { + const hits = response?.hits?.hits ?? []; return hits .map((hit) => { - const cluster = get(hit, '_source'); + const cluster = hit._source as ElasticsearchModifiedSource; if (cluster) { - const indexName = get(hit, '_index', ''); + const indexName = hit._index; const ccs = parseCrossClusterPrefix(indexName); // use CCS whenever we come across it so that we can avoid talking to other monitoring clusters whenever possible diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts similarity index 72% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts index 0f0ba49f229b0..ec7ccd5ddb9af 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts @@ -4,19 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import moment from 'moment'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { ElasticsearchMetric } from '../metrics'; +// @ts-ignore import { createQuery } from '../create_query'; +import { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; -export function handleResponse(response) { - const isEnabled = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.enabled'); - const isAvailable = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.available'); +export function handleResponse(response: ElasticsearchResponse) { + const isEnabled = response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.enabled ?? undefined; + const isAvailable = + response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.available ?? undefined; return isEnabled && isAvailable; } -export async function checkCcrEnabled(req, esIndexPattern) { +export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string) { checkParam(esIndexPattern, 'esIndexPattern in getNodes'); const start = moment.utc(req.payload.timeRange.min).valueOf(); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts similarity index 81% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts index 00e750b17d57b..31a58651ecd3b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts @@ -5,9 +5,14 @@ */ import moment from 'moment'; import _ from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { createQuery } from '../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../metrics'; +import { ElasticsearchResponse, ElasticsearchIndexRecoveryShard } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; /** * Filter out shard activity that we do not care about. @@ -20,8 +25,8 @@ import { ElasticsearchMetric } from '../metrics'; * @param {Number} startMs Start time in milliseconds of the polling window * @returns {boolean} true to keep */ -export function filterOldShardActivity(startMs) { - return (activity) => { +export function filterOldShardActivity(startMs: number) { + return (activity: ElasticsearchIndexRecoveryShard) => { // either it's still going and there is no stop time, or the stop time happened after we started looking for one return !_.isNumber(activity.stop_time_in_millis) || activity.stop_time_in_millis >= startMs; }; @@ -35,9 +40,9 @@ export function filterOldShardActivity(startMs) { * @param {Date} start The start time from the request payload (expected to be of type {@code Date}) * @returns {Object[]} An array of shards representing active shard activity from {@code _source.index_recovery.shards}. */ -export function handleLastRecoveries(resp, start) { - if (resp.hits.hits.length === 1) { - const data = _.get(resp.hits.hits[0], '_source.index_recovery.shards', []).filter( +export function handleLastRecoveries(resp: ElasticsearchResponse, start: number) { + if (resp.hits?.hits.length === 1) { + const data = (resp.hits?.hits[0]?._source.index_recovery?.shards ?? []).filter( filterOldShardActivity(moment.utc(start).valueOf()) ); data.sort((a, b) => b.start_time_in_millis - a.start_time_in_millis); @@ -47,7 +52,7 @@ export function handleLastRecoveries(resp, start) { return []; } -export function getLastRecovery(req, esIndexPattern) { +export function getLastRecovery(req: LegacyRequest, esIndexPattern: string) { checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getLastRecovery'); const start = req.payload.timeRange.min; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts similarity index 79% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts index 71f3633406c9b..29f5a38ca3a21 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts @@ -4,22 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import Bluebird from 'bluebird'; -import { includes, get } from 'lodash'; +import { includes } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { createQuery } from '../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../metrics'; import { ML_SUPPORTED_LICENSES } from '../../../common/constants'; +import { ElasticsearchResponse, ElasticsearchSource } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; /* * Get a listing of jobs along with some metric data to use for the listing */ -export function handleResponse(response) { - const hits = get(response, 'hits.hits', []); - return hits.map((hit) => get(hit, '_source.job_stats')); +export function handleResponse(response: ElasticsearchResponse) { + const hits = response.hits?.hits; + return hits?.map((hit) => hit._source.job_stats) ?? []; } -export function getMlJobs(req, esIndexPattern) { +export function getMlJobs(req: LegacyRequest, esIndexPattern: string) { checkParam(esIndexPattern, 'esIndexPattern in getMlJobs'); const config = req.server.config(); @@ -56,8 +60,12 @@ export function getMlJobs(req, esIndexPattern) { * cardinality isn't guaranteed to be accurate is the issue * but it will be as long as the precision threshold is >= the actual value */ -export function getMlJobsForCluster(req, esIndexPattern, cluster) { - const license = get(cluster, 'license', {}); +export function getMlJobsForCluster( + req: LegacyRequest, + esIndexPattern: string, + cluster: ElasticsearchSource +) { + const license = cluster.license ?? {}; if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) { // ML is supported @@ -80,11 +88,11 @@ export function getMlJobsForCluster(req, esIndexPattern, cluster) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((response) => { - return get(response, 'aggregations.jobs_count.value', 0); + return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => { + return response.aggregations.jobs_count.value ?? 0; }); } // ML is not supported - return Bluebird.resolve(null); + return Promise.resolve(null); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts similarity index 73% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts index 6a0935f2b2d67..3257c5ac36084 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts @@ -5,22 +5,27 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { checkParam } from '../../error_missing_required'; +// @ts-ignore import { createQuery } from '../../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../../metrics'; -import { i18n } from '@kbn/i18n'; +import { ElasticsearchResponse } from '../../../../common/types/es'; +import { LegacyRequest } from '../../../types'; -export function handleResponse(shardStats, indexUuid) { - return (response) => { - const indexStats = get(response, 'hits.hits[0]._source.index_stats'); - const primaries = get(indexStats, 'primaries'); - const total = get(indexStats, 'total'); +export function handleResponse(shardStats: any, indexUuid: string) { + return (response: ElasticsearchResponse) => { + const indexStats = response.hits?.hits[0]?._source.index_stats; + const primaries = indexStats?.primaries; + const total = indexStats?.total; const stats = { - documents: get(primaries, 'docs.count'), + documents: primaries?.docs?.count, dataSize: { - primaries: get(primaries, 'store.size_in_bytes'), - total: get(total, 'store.size_in_bytes'), + primaries: primaries?.store?.size_in_bytes, + total: total?.store?.size_in_bytes, }, }; @@ -55,10 +60,15 @@ export function handleResponse(shardStats, indexUuid) { } export function getIndexSummary( - req, - esIndexPattern, - shardStats, - { clusterUuid, indexUuid, start, end } + req: LegacyRequest, + esIndexPattern: string, + shardStats: any, + { + clusterUuid, + indexUuid, + start, + end, + }: { clusterUuid: string; indexUuid: string; start: number; end: number } ) { checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndexSummary'); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts similarity index 72% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts index efea687ef8037..bf19fcf8978e9 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts @@ -5,43 +5,54 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { checkParam } from '../../error_missing_required'; +// @ts-ignore import { ElasticsearchMetric } from '../../metrics'; +// @ts-ignore import { createQuery } from '../../create_query'; +// @ts-ignore import { calculateRate } from '../../calculate_rate'; +// @ts-ignore import { getUnassignedShards } from '../shards'; -import { i18n } from '@kbn/i18n'; - -export function handleResponse(resp, min, max, shardStats) { +import { ElasticsearchResponse } from '../../../../common/types/es'; +import { LegacyRequest } from '../../../types'; + +export function handleResponse( + resp: ElasticsearchResponse, + min: number, + max: number, + shardStats: any +) { // map the hits - const hits = get(resp, 'hits.hits', []); + const hits = resp?.hits?.hits ?? []; return hits.map((hit) => { - const stats = get(hit, '_source.index_stats'); - const earliestStats = get(hit, 'inner_hits.earliest.hits.hits[0]._source.index_stats'); + const stats = hit._source.index_stats; + const earliestStats = hit.inner_hits?.earliest?.hits?.hits[0]?._source.index_stats; const rateOptions = { - hitTimestamp: get(hit, '_source.timestamp'), - earliestHitTimestamp: get(hit, 'inner_hits.earliest.hits.hits[0]._source.timestamp'), + hitTimestamp: hit._source.timestamp, + earliestHitTimestamp: hit.inner_hits?.earliest?.hits?.hits[0]?._source.timestamp, timeWindowMin: min, timeWindowMax: max, }; - const earliestIndexingHit = get(earliestStats, 'primaries.indexing'); + const earliestIndexingHit = earliestStats?.primaries?.indexing; const { rate: indexRate } = calculateRate({ - latestTotal: get(stats, 'primaries.indexing.index_total'), - earliestTotal: get(earliestIndexingHit, 'index_total'), + latestTotal: stats?.primaries?.indexing?.index_total, + earliestTotal: earliestIndexingHit?.index_total, ...rateOptions, }); - const earliestSearchHit = get(earliestStats, 'total.search'); + const earliestSearchHit = earliestStats?.total?.search; const { rate: searchRate } = calculateRate({ - latestTotal: get(stats, 'total.search.query_total'), - earliestTotal: get(earliestSearchHit, 'query_total'), + latestTotal: stats?.total?.search?.query_total, + earliestTotal: earliestSearchHit?.query_total, ...rateOptions, }); - const shardStatsForIndex = get(shardStats, ['indices', stats.index]); - + const shardStatsForIndex = get(shardStats, ['indices', stats?.index ?? '']); let status; let statusSort; let unassignedShards; @@ -65,10 +76,10 @@ export function handleResponse(resp, min, max, shardStats) { } return { - name: stats.index, + name: stats?.index, status, - doc_count: get(stats, 'primaries.docs.count'), - data_size: get(stats, 'total.store.size_in_bytes'), + doc_count: stats?.primaries?.docs?.count, + data_size: stats?.total?.store?.size_in_bytes, index_rate: indexRate, search_rate: searchRate, unassigned_shards: unassignedShards, @@ -78,9 +89,14 @@ export function handleResponse(resp, min, max, shardStats) { } export function buildGetIndicesQuery( - esIndexPattern, - clusterUuid, - { start, end, size, showSystemIndices = false } + esIndexPattern: string, + clusterUuid: string, + { + start, + end, + size, + showSystemIndices = false, + }: { start: number; end: number; size: number; showSystemIndices: boolean } ) { const filters = []; if (!showSystemIndices) { @@ -134,7 +150,12 @@ export function buildGetIndicesQuery( }; } -export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { +export function getIndices( + req: LegacyRequest, + esIndexPattern: string, + showSystemIndices: boolean = false, + shardStats: any +) { checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); const { min: start, max: end } = req.payload.timeRange; @@ -145,7 +166,7 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard start, end, showSystemIndices, - size: config.get('monitoring.ui.max_bucket_size'), + size: parseInt(config.get('monitoring.ui.max_bucket_size') || '', 10), }); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts similarity index 59% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts index 06f5d5488a1ae..bf7471d77b616 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts @@ -5,35 +5,49 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { checkParam } from '../../error_missing_required'; +// @ts-ignore import { createQuery } from '../../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../../metrics'; +// @ts-ignore import { getDefaultNodeFromId } from './get_default_node_from_id'; +// @ts-ignore import { calculateNodeType } from './calculate_node_type'; +// @ts-ignore import { getNodeTypeClassLabel } from './get_node_type_class_label'; -import { i18n } from '@kbn/i18n'; +import { + ElasticsearchSource, + ElasticsearchResponse, + ElasticsearchLegacySource, + ElasticsearchMetricbeatNode, +} from '../../../../common/types/es'; +import { LegacyRequest } from '../../../types'; -export function handleResponse(clusterState, shardStats, nodeUuid) { - return (response) => { +export function handleResponse( + clusterState: ElasticsearchSource['cluster_state'], + shardStats: any, + nodeUuid: string +) { + return (response: ElasticsearchResponse) => { let nodeSummary = {}; - const nodeStatsHits = get(response, 'hits.hits', []); - const nodes = nodeStatsHits.map((hit) => - get(hit, '_source.elasticsearch.node', hit._source.source_node) - ); // using [0] value because query results are sorted desc per timestamp + const nodeStatsHits = response.hits?.hits ?? []; + const nodes: Array< + ElasticsearchLegacySource['source_node'] | ElasticsearchMetricbeatNode + > = nodeStatsHits.map((hit) => hit._source.elasticsearch?.node || hit._source.source_node); // using [0] value because query results are sorted desc per timestamp const node = nodes[0] || getDefaultNodeFromId(nodeUuid); const sourceStats = - get(response, 'hits.hits[0]._source.elasticsearch.node.stats') || - get(response, 'hits.hits[0]._source.node_stats'); - const clusterNode = get(clusterState, ['nodes', nodeUuid]); + response.hits?.hits[0]?._source.elasticsearch?.node?.stats || + response.hits?.hits[0]?._source.node_stats; + const clusterNode = + clusterState && clusterState.nodes ? clusterState.nodes[nodeUuid] : undefined; const stats = { resolver: nodeUuid, - node_ids: nodes.map((node) => node.id || node.uuid), + node_ids: nodes.map((_node) => node.id || node.uuid), attributes: node.attributes, - transport_address: get( - response, - 'hits.hits[0]._source.service.address', - node.transport_address - ), + transport_address: response.hits?.hits[0]?._source.service?.address || node.transport_address, name: node.name, type: node.type, }; @@ -48,22 +62,19 @@ export function handleResponse(clusterState, shardStats, nodeUuid) { nodeSummary = { type: nodeType, - nodeTypeLabel: nodeTypeLabel, - nodeTypeClass: nodeTypeClass, + nodeTypeLabel, + nodeTypeClass, totalShards: _shardStats.shardCount, indexCount: _shardStats.indexCount, - documents: get(sourceStats, 'indices.docs.count'), + documents: sourceStats?.indices?.docs?.count, dataSize: - get(sourceStats, 'indices.store.size_in_bytes') || - get(sourceStats, 'indices.store.size.bytes'), + sourceStats?.indices?.store?.size_in_bytes || sourceStats?.indices?.store?.size?.bytes, freeSpace: - get(sourceStats, 'fs.total.available_in_bytes') || - get(sourceStats, 'fs.summary.available.bytes'), + sourceStats?.fs?.total?.available_in_bytes || sourceStats?.fs?.summary?.available?.bytes, totalSpace: - get(sourceStats, 'fs.total.total_in_bytes') || get(sourceStats, 'fs.summary.total.bytes'), + sourceStats?.fs?.total?.total_in_bytes || sourceStats?.fs?.summary?.total?.bytes, usedHeap: - get(sourceStats, 'jvm.mem.heap_used_percent') || - get(sourceStats, 'jvm.mem.heap.used.pct'), + sourceStats?.jvm?.mem?.heap_used_percent || sourceStats?.jvm?.mem?.heap?.used?.pct, status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', { defaultMessage: 'Online', }), @@ -89,11 +100,16 @@ export function handleResponse(clusterState, shardStats, nodeUuid) { } export function getNodeSummary( - req, - esIndexPattern, - clusterState, - shardStats, - { clusterUuid, nodeUuid, start, end } + req: LegacyRequest, + esIndexPattern: string, + clusterState: ElasticsearchSource['cluster_state'], + shardStats: any, + { + clusterUuid, + nodeUuid, + start, + end, + }: { clusterUuid: string; nodeUuid: string; start: number; end: number } ) { checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getNodeSummary'); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts index ac4fcea6150a0..1e412d2303cc0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts @@ -5,13 +5,21 @@ */ import moment from 'moment'; +// @ts-ignore import { checkParam } from '../../../error_missing_required'; +// @ts-ignore import { createQuery } from '../../../create_query'; +// @ts-ignore import { calculateAuto } from '../../../calculate_auto'; +// @ts-ignore import { ElasticsearchMetric } from '../../../metrics'; +// @ts-ignore import { getMetricAggs } from './get_metric_aggs'; import { handleResponse } from './handle_response'; +// @ts-ignore import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_metrics'; +import { LegacyRequest } from '../../../../types'; +import { ElasticsearchModifiedSource } from '../../../../../common/types/es'; /* Run an aggregation on node_stats to get stat data for the selected time * range for all the active nodes. Every option is a key to a configuration @@ -30,7 +38,13 @@ import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_me * @param {Object} nodesShardCount: per-node information about shards * @return {Array} node info combined with metrics for each node from handle_response */ -export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, nodesShardCount) { +export async function getNodes( + req: LegacyRequest, + esIndexPattern: string, + pageOfNodes: Array<{ uuid: string }>, + clusterStats: ElasticsearchModifiedSource, + nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } +) { checkParam(esIndexPattern, 'esIndexPattern in getNodes'); const start = moment.utc(req.payload.timeRange.min).valueOf(); @@ -45,7 +59,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n const min = start; const bucketSize = Math.max( - config.get('monitoring.ui.min_interval_seconds'), + parseInt(config.get('monitoring.ui.min_interval_seconds') as string, 10), calculateAuto(100, duration).asSeconds() ); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts similarity index 57% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts index 3f82e8ec3e646..3e248a06318da 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts @@ -6,8 +6,11 @@ import { get } from 'lodash'; import { mapNodesInfo } from './map_nodes_info'; +// @ts-ignore import { mapNodesMetrics } from './map_nodes_metrics'; +// @ts-ignore import { uncovertMetricNames } from '../../convert_metric_names'; +import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../../../common/types/es'; /* * Process the response from the get_nodes query @@ -18,18 +21,18 @@ import { uncovertMetricNames } from '../../convert_metric_names'; * @return {Array} node info combined with metrics for each node */ export function handleResponse( - response, - clusterStats, - nodesShardCount, - pageOfNodes, + response: ElasticsearchResponse, + clusterStats: ElasticsearchModifiedSource | undefined, + nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } | undefined, + pageOfNodes: Array<{ uuid: string }>, timeOptions = {} ) { if (!get(response, 'hits.hits')) { return []; } - const nodeHits = get(response, 'hits.hits', []); - const nodesInfo = mapNodesInfo(nodeHits, clusterStats, nodesShardCount); + const nodeHits = response.hits?.hits ?? []; + const nodesInfo: { [key: string]: any } = mapNodesInfo(nodeHits, clusterStats, nodesShardCount); /* * Every node bucket is an object with a field for nodeId and fields for @@ -37,19 +40,29 @@ export function handleResponse( * with a sub-object for all the metrics buckets */ const nodeBuckets = get(response, 'aggregations.nodes.buckets', []); - const metricsForNodes = nodeBuckets.reduce((accum, { key: nodeId, by_date: byDate }) => { - return { - ...accum, - [nodeId]: uncovertMetricNames(byDate), - }; - }, {}); - const nodesMetrics = mapNodesMetrics(metricsForNodes, nodesInfo, timeOptions); // summarize the metrics of online nodes + const metricsForNodes = nodeBuckets.reduce( + ( + accum: { [nodeId: string]: any }, + { key: nodeId, by_date: byDate }: { key: string; by_date: any } + ) => { + return { + ...accum, + [nodeId]: uncovertMetricNames(byDate), + }; + }, + {} + ); + const nodesMetrics: { [key: string]: any } = mapNodesMetrics( + metricsForNodes, + nodesInfo, + timeOptions + ); // summarize the metrics of online nodes // nodesInfo is the source of truth for the nodeIds, where nodesMetrics will lack metrics for offline nodes const nodes = pageOfNodes.map((node) => ({ ...node, - ...nodesInfo[node.uuid], - ...nodesMetrics[node.uuid], + ...(nodesInfo && nodesInfo[node.uuid] ? nodesInfo[node.uuid] : {}), + ...(nodesMetrics && nodesMetrics[node.uuid] ? nodesMetrics[node.uuid] : {}), resolver: node.uuid, })); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js deleted file mode 100644 index 317c1cddf57ae..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js +++ /dev/null @@ -1,46 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isUndefined } from 'lodash'; -import { calculateNodeType, getNodeTypeClassLabel } from '../'; - -/** - * @param {Array} nodeHits: info about each node from the hits in the get_nodes query - * @param {Object} clusterStats: cluster stats from cluster state document - * @param {Object} nodesShardCount: per-node information about shards - * @return {Object} summarized info about each node keyed by nodeId - */ -export function mapNodesInfo(nodeHits, clusterStats, nodesShardCount) { - const clusterState = get(clusterStats, 'cluster_state', { nodes: {} }); - - return nodeHits.reduce((prev, node) => { - const sourceNode = get(node, '_source.source_node') || get(node, '_source.elasticsearch.node'); - - const calculatedNodeType = calculateNodeType(sourceNode, get(clusterState, 'master_node')); - const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel( - sourceNode, - calculatedNodeType - ); - const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid || sourceNode.id])); - - return { - ...prev, - [sourceNode.uuid || sourceNode.id]: { - name: sourceNode.name, - transport_address: sourceNode.transport_address, - type: nodeType, - isOnline, - nodeTypeLabel: nodeTypeLabel, - nodeTypeClass: nodeTypeClass, - shardCount: get( - nodesShardCount, - `nodes[${sourceNode.uuid || sourceNode.id}].shardCount`, - 0 - ), - }, - }; - }, {}); -} diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts new file mode 100644 index 0000000000000..a2e80c9ca1d96 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isUndefined } from 'lodash'; +// @ts-ignore +import { calculateNodeType } from '../calculate_node_type'; +// @ts-ignore +import { getNodeTypeClassLabel } from '../get_node_type_class_label'; +import { + ElasticsearchResponseHit, + ElasticsearchModifiedSource, +} from '../../../../../common/types/es'; + +/** + * @param {Array} nodeHits: info about each node from the hits in the get_nodes query + * @param {Object} clusterStats: cluster stats from cluster state document + * @param {Object} nodesShardCount: per-node information about shards + * @return {Object} summarized info about each node keyed by nodeId + */ +export function mapNodesInfo( + nodeHits: ElasticsearchResponseHit[], + clusterStats?: ElasticsearchModifiedSource, + nodesShardCount?: { nodes: { [nodeId: string]: { shardCount: number } } } +) { + const clusterState = clusterStats?.cluster_state ?? { nodes: {} }; + + return nodeHits.reduce((prev, node) => { + const sourceNode = node._source.source_node || node._source.elasticsearch?.node; + + const calculatedNodeType = calculateNodeType(sourceNode, clusterState.master_node); + const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel( + sourceNode, + calculatedNodeType + ); + const uuid = sourceNode?.uuid ?? sourceNode?.id ?? undefined; + if (!uuid) { + return prev; + } + const isOnline = !isUndefined(clusterState.nodes ? clusterState.nodes[uuid] : undefined); + + return { + ...prev, + [uuid]: { + name: sourceNode?.name, + transport_address: sourceNode?.transport_address, + type: nodeType, + isOnline, + nodeTypeLabel, + nodeTypeClass, + shardCount: nodesShardCount?.nodes[uuid]?.shardCount ?? 0, + }, + }; + }, {}); +} diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts index 40412c03b0ef9..ed37b56b7ad05 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts @@ -4,22 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +// @ts-ignore import { checkParam } from '../../error_missing_required'; +// @ts-ignore import { createQuery } from '../../create_query'; +// @ts-ignore import { ElasticsearchMetric } from '../../metrics'; +import { ElasticsearchResponse, ElasticsearchLegacySource } from '../../../../common/types/es'; +import { LegacyRequest } from '../../../types'; -export function handleResponse(response) { - const hits = get(response, 'hits.hits'); +export function handleResponse(response: ElasticsearchResponse) { + const hits = response.hits?.hits; if (!hits) { return []; } // deduplicate any shards from earlier days with the same cluster state state_uuid - const uniqueShards = new Set(); + const uniqueShards = new Set(); // map into object with shard and source properties - return hits.reduce((shards, hit) => { + return hits.reduce((shards: Array, hit) => { const shard = hit._source.shard; if (shard) { @@ -37,9 +41,13 @@ export function handleResponse(response) { } export function getShardAllocation( - req, - esIndexPattern, - { shardFilter, stateUuid, showSystemIndices = false } + req: LegacyRequest, + esIndexPattern: string, + { + shardFilter, + stateUuid, + showSystemIndices = false, + }: { shardFilter: any; stateUuid: string; showSystemIndices: boolean } ) { checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardAllocation'); diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts index 0e8903908a55e..4a0c598eec307 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts @@ -9,10 +9,11 @@ import { merge } from 'lodash'; import { checkParam } from '../error_missing_required'; // @ts-ignore import { calculateAvailability } from '../calculate_availability'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; export function handleResponse(resp: ElasticsearchResponse) { - const source = resp.hits?.hits[0]._source.kibana_stats; + const source = resp.hits?.hits[0]?._source.kibana_stats; const kibana = source?.kibana; return merge(kibana, { availability: calculateAvailability(source?.timestamp), diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts index ead8764607786..cdf2277424002 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts @@ -9,7 +9,8 @@ import { merge } from 'lodash'; import { checkParam } from '../error_missing_required'; // @ts-ignore import { calculateAvailability } from '../calculate_availability'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; export function handleResponse(resp: ElasticsearchResponse) { const source = resp.hits?.hits[0]?._source?.logstash_stats; diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts index 96419ceb4cc70..1556b44f73804 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts @@ -8,7 +8,8 @@ import { createQuery } from '../create_query'; // @ts-ignore import { LogstashMetric } from '../metrics'; -import { LegacyRequest, ElasticsearchResponse } from '../../types'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponse } from '../../../common/types/es'; export async function getPipelineStateDocument( req: LegacyRequest, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts similarity index 72% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 9f69ea1465c2d..6f41455ebca6a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -7,11 +7,15 @@ import { schema } from '@kbn/config-schema'; import moment from 'moment'; import { get, groupBy } from 'lodash'; +// @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; +// @ts-ignore import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; +import { ElasticsearchResponse, ElasticsearchSource } from '../../../../../common/types/es'; +import { LegacyRequest } from '../../../../types'; -function getBucketScript(max, min) { +function getBucketScript(max: string, min: string) { return { bucket_script: { buckets_path: { @@ -23,7 +27,13 @@ function getBucketScript(max, min) { }; } -function buildRequest(req, config, esIndexPattern) { +function buildRequest( + req: LegacyRequest, + config: { + get: (key: string) => string | undefined; + }, + esIndexPattern: string +) { const min = moment.utc(req.payload.timeRange.min).valueOf(); const max = moment.utc(req.payload.timeRange.max).valueOf(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); @@ -168,7 +178,12 @@ function buildRequest(req, config, esIndexPattern) { }; } -export function ccrRoute(server) { +export function ccrRoute(server: { + route: (p: any) => void; + config: () => { + get: (key: string) => string | undefined; + }; +}) { server.route({ method: 'POST', path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr', @@ -186,14 +201,14 @@ export function ccrRoute(server) { }), }, }, - async handler(req) { + async handler(req: LegacyRequest) { const config = server.config(); const ccs = req.payload.ccs; const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); try { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - const response = await callWithRequest( + const response: ElasticsearchResponse = await callWithRequest( req, 'search', buildRequest(req, config, esIndexPattern) @@ -203,50 +218,72 @@ export function ccrRoute(server) { return { data: [] }; } - const fullStats = get(response, 'hits.hits').reduce((accum, hit) => { - const innerHits = get(hit, 'inner_hits.by_shard.hits.hits'); - const innerHitsSource = innerHits.map((innerHit) => get(innerHit, '_source.ccr_stats')); - const grouped = groupBy( - innerHitsSource, - (stat) => `${stat.follower_index}:${stat.shard_id}` - ); + const fullStats: { + [key: string]: Array>; + } = + response.hits?.hits.reduce((accum, hit) => { + const innerHits = hit.inner_hits?.by_shard.hits?.hits ?? []; + const innerHitsSource = innerHits.map( + (innerHit) => + innerHit._source.ccr_stats as NonNullable + ); + const grouped = groupBy( + innerHitsSource, + (stat) => `${stat.follower_index}:${stat.shard_id}` + ); - return { - ...accum, - ...grouped, - }; - }, {}); + return { + ...accum, + ...grouped, + }; + }, {}) ?? {}; - const buckets = get(response, 'aggregations.by_follower_index.buckets'); - const data = buckets.reduce((accum, bucket) => { + const buckets = response.aggregations.by_follower_index.buckets; + const data = buckets.reduce((accum: any, bucket: any) => { const leaderIndex = get(bucket, 'leader_index.buckets[0].key'); const remoteCluster = get( bucket, 'leader_index.buckets[0].remote_cluster.buckets[0].key' ); const follows = remoteCluster ? `${leaderIndex} on ${remoteCluster}` : leaderIndex; - const stat = { + const stat: { + [key: string]: any; + shards: Array<{ + error?: string; + opsSynced: number; + syncLagTime: number; + syncLagOps: number; + }>; + } = { id: bucket.key, index: bucket.key, follows, + shards: [], + error: undefined, + opsSynced: undefined, + syncLagTime: undefined, + syncLagOps: undefined, }; - stat.shards = get(bucket, 'by_shard_id.buckets').reduce((accum, shardBucket) => { - const fullStat = get(fullStats[`${bucket.key}:${shardBucket.key}`], '[0]', {}); - const shardStat = { - shardId: shardBucket.key, - error: fullStat.read_exceptions.length - ? fullStat.read_exceptions[0].exception.type - : null, - opsSynced: get(shardBucket, 'ops_synced.value'), - syncLagTime: fullStat.time_since_last_read_millis, - syncLagOps: get(shardBucket, 'lag_ops.value'), - syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'), - syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'), - }; - accum.push(shardStat); - return accum; - }, []); + stat.shards = get(bucket, 'by_shard_id.buckets').reduce( + (accum2: any, shardBucket: any) => { + const fullStat = fullStats[`${bucket.key}:${shardBucket.key}`][0] ?? {}; + const shardStat = { + shardId: shardBucket.key, + error: fullStat.read_exceptions?.length + ? fullStat.read_exceptions[0].exception?.type + : null, + opsSynced: get(shardBucket, 'ops_synced.value'), + syncLagTime: fullStat.time_since_last_read_millis, + syncLagOps: get(shardBucket, 'lag_ops.value'), + syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'), + syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'), + }; + accum2.push(shardStat); + return accum2; + }, + [] + ); stat.error = (stat.shards.find((shard) => shard.error) || {}).error; stat.opsSynced = stat.shards.reduce((sum, { opsSynced }) => sum + opsSynced, 0); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts similarity index 82% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index 92458a31c6bd8..fd834cff29aa3 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; +// @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; +// @ts-ignore import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +// @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; +import { ElasticsearchResponse } from '../../../../../common/types/es'; +import { LegacyRequest } from '../../../../types'; -function getFormattedLeaderIndex(leaderIndex) { +function getFormattedLeaderIndex(leaderIndex: string) { let leader = leaderIndex; if (leader.includes(':')) { const leaderSplit = leader.split(':'); @@ -21,7 +25,7 @@ function getFormattedLeaderIndex(leaderIndex) { return leader; } -async function getCcrStat(req, esIndexPattern, filters) { +async function getCcrStat(req: LegacyRequest, esIndexPattern: string, filters: unknown[]) { const min = moment.utc(req.payload.timeRange.min).valueOf(); const max = moment.utc(req.payload.timeRange.max).valueOf(); @@ -68,7 +72,7 @@ async function getCcrStat(req, esIndexPattern, filters) { return await callWithRequest(req, 'search', params); } -export function ccrShardRoute(server) { +export function ccrShardRoute(server: { route: (p: any) => void; config: () => {} }) { server.route({ method: 'POST', path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr/{index}/shard/{shardId}', @@ -88,7 +92,7 @@ export function ccrShardRoute(server) { }), }, }, - async handler(req) { + async handler(req: LegacyRequest) { const config = server.config(); const index = req.params.index; const shardId = req.params.shardId; @@ -120,7 +124,7 @@ export function ccrShardRoute(server) { ]; try { - const [metrics, ccrResponse] = await Promise.all([ + const [metrics, ccrResponse]: [unknown, ElasticsearchResponse] = await Promise.all([ getMetrics( req, esIndexPattern, @@ -133,18 +137,15 @@ export function ccrShardRoute(server) { getCcrStat(req, esIndexPattern, filters), ]); - const stat = get(ccrResponse, 'hits.hits[0]._source.ccr_stats', {}); - const oldestStat = get( - ccrResponse, - 'hits.hits[0].inner_hits.oldest.hits.hits[0]._source.ccr_stats', - {} - ); + const stat = ccrResponse.hits?.hits[0]?._source.ccr_stats ?? {}; + const oldestStat = + ccrResponse.hits?.hits[0].inner_hits?.oldest.hits?.hits[0]?._source.ccr_stats ?? {}; return { metrics, stat, - formattedLeader: getFormattedLeaderIndex(stat.leader_index), - timestamp: get(ccrResponse, 'hits.hits[0]._source.timestamp'), + formattedLeader: getFormattedLeaderIndex(stat.leader_index ?? ''), + timestamp: ccrResponse.hits?.hits[0]?._source.timestamp, oldestStat, }; } catch (err) { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 47ac3beb8c390..dd6ec9c7930e5 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -17,7 +17,6 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { CloudSetup } from '../../cloud/server'; -import { ElasticsearchSource } from '../common/types/es'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -118,26 +117,3 @@ export interface LegacyServer { }; }; } - -export interface ElasticsearchResponse { - hits?: { - hits: ElasticsearchResponseHit[]; - total: { - value: number; - }; - }; -} - -export interface ElasticsearchResponseHit { - _source: ElasticsearchSource; - inner_hits?: { - [field: string]: { - hits?: { - hits: ElasticsearchResponseHit[]; - total: { - value: number; - }; - }; - }; - }; -} From 8b1a228c294bb1b8ff7c8bab2fdb3db34983cb66 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 20 Jan 2021 10:53:01 -0800 Subject: [PATCH 17/28] [Alerting] Migrate Actions, Alerts, Stack Alerts and TriggersActionsUI plugins to TS project references (#88556) * [Alerting] Migrate Actions plugin to TS project references * alerts plugin ts migration * triggers_actions_ui plugin ts migration * fixed build * fixed build --- .../examples/alerting_example/tsconfig.json | 2 +- .../server/lib/ensure_sufficient_license.ts | 3 +- x-pack/plugins/actions/tsconfig.json | 27 +++++++++++++++++ .../alerts/server/saved_objects/migrations.ts | 6 ++-- x-pack/plugins/alerts/tsconfig.json | 30 +++++++++++++++++++ x-pack/plugins/stack_alerts/tsconfig.json | 25 ++++++++++++++++ .../plugins/triggers_actions_ui/tsconfig.json | 29 ++++++++++++++++++ x-pack/test/tsconfig.json | 4 +++ x-pack/tsconfig.json | 10 ++++++- x-pack/tsconfig.refs.json | 4 +++ 10 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/actions/tsconfig.json create mode 100644 x-pack/plugins/alerts/tsconfig.json create mode 100644 x-pack/plugins/stack_alerts/tsconfig.json create mode 100644 x-pack/plugins/triggers_actions_ui/tsconfig.json diff --git a/x-pack/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json index 95d42d40aceb3..99e0f1f0e7c9e 100644 --- a/x-pack/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -9,7 +9,7 @@ "public/**/*.tsx", "server/**/*.ts", "common/**/*.ts", - "../../../typings/**/*", + "../../typings/**/*", ], "exclude": [], "references": [ diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts index f22e87a58ec7f..c4ed47b3398df 100644 --- a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts @@ -6,9 +6,10 @@ import { ActionType } from '../types'; import { LICENSE_TYPE } from '../../../licensing/common/types'; import { ServerLogActionTypeId, IndexActionTypeId } from '../builtin_action_types'; -import { CASE_ACTION_TYPE_ID } from '../../../case/server'; import { ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; +export const CASE_ACTION_TYPE_ID = '.case'; + const ACTIONS_SCOPED_WITHIN_STACK = new Set([ ServerLogActionTypeId, IndexActionTypeId, diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json new file mode 100644 index 0000000000000..d5c1105c99ad0 --- /dev/null +++ b/x-pack/plugins/actions/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "common/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../event_log/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 1b9c5dac23b88..76696d11d5f03 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -11,11 +11,9 @@ import { } from '../../../../../src/core/server'; import { RawAlert } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; -import { - APP_ID as SIEM_APP_ID, - SERVER_APP_ID as SIEM_SERVER_APP_ID, -} from '../../../security_solution/common/constants'; +const SIEM_APP_ID = 'securitySolution'; +const SIEM_SERVER_APP_ID = 'siem'; export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; type AlertMigration = ( diff --git a/x-pack/plugins/alerts/tsconfig.json b/x-pack/plugins/alerts/tsconfig.json new file mode 100644 index 0000000000000..86ab00faeb5ad --- /dev/null +++ b/x-pack/plugins/alerts/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*", + "common/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../event_log/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json new file mode 100644 index 0000000000000..ad047001f8d89 --- /dev/null +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + "server/**/*.json", + "public/**/*", + "common/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json new file mode 100644 index 0000000000000..715ed6848d9b7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + "public/**/*", + "common/**/*", + "config.ts", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cfe328236cd36..699ff64af3f88 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -36,6 +36,8 @@ { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../plugins/actions/tsconfig.json"}, + { "path": "../plugins/alerts/tsconfig.json"}, { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/global_search/tsconfig.json" }, @@ -46,10 +48,12 @@ { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/spaces/tsconfig.json" }, { "path": "../plugins/security/tsconfig.json" }, { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "../plugins/stack_alerts/tsconfig.json" }, { "path": "../plugins/beats_management/tsconfig.json" }, { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 812ead39ba412..ae12773023663 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../tsconfig.base.json", "include": ["mocks.ts", "typings/**/*", "plugins/**/*", "tasks/**/*"], "exclude": [ + "plugins/actions/**/*", + "plugins/alerts/**/*", "plugins/apm/e2e/cypress/**/*", "plugins/apm/ftr_e2e/**/*", "plugins/apm/scripts/**/*", @@ -21,10 +23,12 @@ "plugins/task_manager/**/*", "plugins/telemetry_collection_xpack/**/*", "plugins/translations/**/*", + "plugins/triggers_actions_ui/**/*", "plugins/ui_actions_enhanced/**/*", "plugins/vis_type_timeseries_enhanced/**/*", "plugins/spaces/**/*", "plugins/security/**/*", + "plugins/stack_alerts/**/*", "plugins/encrypted_saved_objects/**/*", "plugins/beats_management/**/*", "plugins/cloud/**/*", @@ -92,6 +96,10 @@ { "path": "./plugins/beats_management/tsconfig.json" }, { "path": "./plugins/cloud/tsconfig.json" }, { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" } + { "path": "./plugins/global_search_bar/tsconfig.json" }, + { "path": "./plugins/actions/tsconfig.json"}, + { "path": "./plugins/alerts/tsconfig.json"}, + { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, + { "path": "./plugins/stack_alerts/tsconfig.json"} ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index edee8e228f769..02623b11ce314 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -1,6 +1,8 @@ { "include": [], "references": [ + { "path": "./plugins/actions/tsconfig.json"}, + { "path": "./plugins/alerts/tsconfig.json"}, { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/console_extensions/tsconfig.json" }, @@ -18,8 +20,10 @@ { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "./plugins/vis_type_timeseries_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, + { "path": "./plugins/triggers_actions_ui/tsconfig.json"}, { "path": "./plugins/spaces/tsconfig.json" }, { "path": "./plugins/security/tsconfig.json" }, + { "path": "./plugins/stack_alerts/tsconfig.json"}, { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, { "path": "./plugins/beats_management/tsconfig.json" }, { "path": "./plugins/cloud/tsconfig.json" }, From f4f6cb687cfe9d55b370503fca556d0bfbb59eab Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 20 Jan 2021 11:07:32 -0800 Subject: [PATCH 18/28] [App Search] Add new encodePathParams helper (fixes unencoded document IDs) (#88648) * Add encodePathParams helper to EnterpriseSearchRequestHandler This helper accomplishes two things: - Fixes 404s from the Enterprise Search server for user-generated IDs with special characters (e.g. ? char) - Allows us to simplify some of our createRequest calls - no longer will we have to grab request.params to create paths, this helper will do so for us * Update AS document route to use new helper - This was the primary view/API I was testing to fix this bug * Update remaining AS routes to use new helper - shorter, saves us a few lines + remove unnecessary payload: params that doesn't actually validate params Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../enterprise_search_request_handler.test.ts | 30 +++++++++++++++-- .../lib/enterprise_search_request_handler.ts | 33 ++++++++++++++++++- .../routes/app_search/analytics.test.ts | 12 ++----- .../server/routes/app_search/analytics.ts | 20 ++++------- .../routes/app_search/credentials.test.ts | 21 ++---------- .../server/routes/app_search/credentials.ts | 16 ++++----- .../routes/app_search/documents.test.ts | 15 ++------- .../server/routes/app_search/documents.ts | 24 +++++--------- .../server/routes/app_search/engines.test.ts | 10 +++--- .../server/routes/app_search/engines.ts | 16 ++++----- .../server/routes/app_search/settings.test.ts | 3 -- .../server/routes/app_search/settings.ts | 8 ++--- 12 files changed, 100 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index e55f997a6b51b..a27932e844177 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -116,16 +116,40 @@ describe('EnterpriseSearchRequestHandler', () => { ); }); - it('correctly encodes paths and query string parameters', async () => { + it('correctly encodes query string parameters', async () => { const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/api/some example', + path: '/api/example', }); await makeAPICall(requestHandler, { query: { 'page[current]': 1 } }); EnterpriseSearchAPI.shouldHaveBeenCalledWith( - 'http://localhost:3002/api/some%20example?page%5Bcurrent%5D=1' + 'http://localhost:3002/api/example?page%5Bcurrent%5D=1' ); }); + + describe('encodePathParams', () => { + it('correctly replaces :pathVariables with request.params', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/examples/:example/some/:id', + }); + await makeAPICall(requestHandler, { params: { example: 'hello', id: 'world' } }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/api/examples/hello/some/world' + ); + }); + + it('correctly encodes path params as URI components', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/examples/:example', + }); + await makeAPICall(requestHandler, { params: { example: 'hello#@/$%^/&[]{}/";world' } }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/api/examples/hello%23%40%2F%24%25%5E%2F%26%5B%5D%7B%7D%2F%22%3Bworld' + ); + }); + }); }); describe('response passing', () => { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 8e4a817a82551..a626198ad9c4d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -65,11 +65,12 @@ export class EnterpriseSearchRequestHandler { ) => { try { // Set up API URL + const encodedPath = this.encodePathParams(path, request.params as Record); const queryParams = { ...(request.query as object), ...params }; const queryString = !this.isEmptyObj(queryParams) ? `?${querystring.stringify(queryParams)}` : ''; - const url = encodeURI(this.enterpriseSearchUrl + path) + queryString; + const url = encodeURI(this.enterpriseSearchUrl) + encodedPath + queryString; // Set up API options const { method } = request.route; @@ -126,6 +127,36 @@ export class EnterpriseSearchRequestHandler { }; } + /** + * This path helper is similar to React Router's generatePath, but much simpler & + * does not use regexes. It enables us to pass a static '/foo/:bar/baz' string to + * createRequest({ path }) and have :bar be automatically replaced by the value of + * request.params.bar. + * It also (very importantly) wraps all URL request params with encodeURIComponent(), + * which is an extra layer of encoding required by the Enterprise Search server in + * order to correctly & safely parse user-generated IDs with special characters in + * their names - just encodeURI alone won't work. + */ + encodePathParams(path: string, params: Record) { + const hasParams = path.includes(':'); + if (!hasParams) { + return path; + } else { + return path + .split('/') + .map((pathPart) => { + const isParam = pathPart.startsWith(':'); + if (!isParam) { + return pathPart; + } else { + const pathParam = pathPart.replace(':', ''); + return encodeURIComponent(params[pathParam]); + } + }) + .join('/'); + } + } + /** * Attempt to grab a usable error body from Enterprise Search - this isn't * always possible because some of our internal endpoints send back blank diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts index 9ede6989052b2..f93b205059b2f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts @@ -27,12 +27,8 @@ describe('analytics routes', () => { }); it('creates a request handler', () => { - mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/analytics/queries', + path: '/as/engines/:engineName/analytics/queries', }); }); @@ -84,12 +80,8 @@ describe('analytics routes', () => { }); it('creates a request handler', () => { - mockRouter.callRoute({ - params: { engineName: 'some-engine', query: 'some-query' }, - }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/analytics/query/some-query', + path: '/as/engines/:engineName/analytics/query/:query', }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts index f7d0786b27fd4..9807ca9ad7917 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts @@ -32,13 +32,9 @@ export function registerAnalyticsRoutes({ query: schema.object(queriesSchema), }, }, - async (context, request, response) => { - const { engineName } = request.params; - - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${engineName}/analytics/queries`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/analytics/queries', + }) ); router.get( @@ -52,12 +48,8 @@ export function registerAnalyticsRoutes({ query: schema.object(querySchema), }, }, - async (context, request, response) => { - const { engineName, query } = request.params; - - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${engineName}/analytics/query/${query}`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/analytics/query/:query', + }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index af498e346529a..f6bcd4adda1fe 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -200,16 +200,8 @@ describe('credentials routes', () => { }); it('creates a request to enterprise search', () => { - const mockRequest = { - params: { - name: 'abc123', - }, - }; - - mockRouter.callRoute(mockRequest); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/abc123', + path: '/as/credentials/:name', }); }); @@ -311,7 +303,6 @@ describe('credentials routes', () => { mockRouter = new MockRouter({ method: 'delete', path: '/api/app_search/credentials/{name}', - payload: 'params', }); registerCredentialsRoutes({ @@ -321,16 +312,8 @@ describe('credentials routes', () => { }); it('creates a request to enterprise search', () => { - const mockRequest = { - params: { - name: 'abc123', - }, - }; - - mockRouter.callRoute(mockRequest); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/abc123', + path: '/as/credentials/:name', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts index a5611af9bba79..29425eedef69f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts @@ -81,11 +81,9 @@ export function registerCredentialsRoutes({ body: tokenSchema, }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/credentials/${request.params.name}`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/:name', + }) ); router.delete( { @@ -96,10 +94,8 @@ export function registerCredentialsRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/credentials/${request.params.name}`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/:name', + }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts index 5f57db40cd7e6..c12a2e69057d4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -27,13 +27,8 @@ describe('documents routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - body: { documents: [{ foo: 'bar' }] }, - }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/documents/new', + path: '/as/engines/:engineName/documents/new', }); }); @@ -79,10 +74,8 @@ describe('document routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/documents/1', + path: '/as/engines/:engineName/documents/:documentId', }); }); }); @@ -104,10 +97,8 @@ describe('document routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/documents/1', + path: '/as/engines/:engineName/documents/:documentId', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts index 60cd64b32479c..665691c3a9ea3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -24,11 +24,9 @@ export function registerDocumentsRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${request.params.engineName}/documents/new`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/:engineName/documents/new`, + }) ); } @@ -46,11 +44,9 @@ export function registerDocumentRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/:engineName/documents/:documentId`, + }) ); router.delete( { @@ -62,10 +58,8 @@ export function registerDocumentRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/:engineName/documents/:documentId`, + }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index ed6847a029100..9755fff02f738 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -59,6 +59,7 @@ describe('engine routes', () => { describe('hasValidData', () => { it('should correctly validate that the response has data', () => { + mockRequestHandler.createRequest.mockClear(); const response = { meta: { page: { @@ -73,6 +74,7 @@ describe('engine routes', () => { }); it('should correctly validate that a response does not have data', () => { + mockRequestHandler.createRequest.mockClear(); const response = {}; mockRouter.callRoute(mockRequest); @@ -125,10 +127,8 @@ describe('engine routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({ params: { name: 'some-engine' } }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/details', + path: '/as/engines/:name/details', }); }); }); @@ -150,10 +150,8 @@ describe('engine routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({ params: { name: 'some-engine' } }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/some-engine/overview_metrics', + path: '/as/engines/:name/overview_metrics', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index f9169d8795f4b..c0bbc40ff8d2d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -54,11 +54,9 @@ export function registerEnginesRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${request.params.name}/details`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/:name/details`, + }) ); router.get( { @@ -69,10 +67,8 @@ export function registerEnginesRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/${request.params.name}/overview_metrics`, - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/:name/overview_metrics`, + }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts index be3b2632eb67d..613ecee90d989 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -26,8 +26,6 @@ describe('log settings routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({}); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/log_settings', }); @@ -52,7 +50,6 @@ describe('log settings routes', () => { }); it('creates a request to enterprise search', () => { - mockRouter.callRoute({}); expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/log_settings', }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts index f9684cdbc060a..bec56c9e3de08 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts @@ -40,10 +40,8 @@ export function registerSettingsRoutes({ }), }, }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: '/as/log_settings', - })(context, request, response); - } + enterpriseSearchRequestHandler.createRequest({ + path: '/as/log_settings', + }) ); } From 15f05b51ff2b6a732b6b28f9af82f3e14a60665c Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 20 Jan 2021 14:57:37 -0500 Subject: [PATCH 19/28] [App Search] Wired up configurable Sort and Facets in Documents View (#88764) --- .../build_search_ui_config.test.ts | 53 +++++--- .../build_search_ui_config.ts | 13 +- .../build_sort_options.test.ts | 63 ++++++++++ .../search_experience/build_sort_options.ts | 29 +++++ .../documents/search_experience/constants.ts | 25 ++++ .../search_experience/search_experience.scss | 8 ++ .../search_experience.test.tsx | 78 +++++++++--- .../search_experience/search_experience.tsx | 69 ++++++++--- .../documents/search_experience/types.ts | 18 +++ .../search_experience/views/index.ts | 1 + .../views/multi_checkbox_facets_view.test.tsx | 99 +++++++++++++++ .../views/multi_checkbox_facets_view.tsx | 114 ++++++++++++++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 14 files changed, 523 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts index dd52f6b8227ba..a87b73bd4e143 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts @@ -15,24 +15,49 @@ describe('buildSearchUIConfig', () => { foo: 'text' as SchemaTypes, bar: 'number' as SchemaTypes, }; + const fields = { + filterFields: ['fieldA', 'fieldB'], + sortFields: [], + }; - const config = buildSearchUIConfig(connector, schema); - expect(config.apiConnector).toEqual(connector); - expect(config.searchQuery.result_fields).toEqual({ - bar: { - raw: {}, - snippet: { - fallback: true, - size: 300, - }, + const config = buildSearchUIConfig(connector, schema, fields); + expect(config).toEqual({ + alwaysSearchOnInitialLoad: true, + apiConnector: connector, + initialState: { + sortDirection: 'desc', + sortField: 'id', }, - foo: { - raw: {}, - snippet: { - fallback: true, - size: 300, + searchQuery: { + disjunctiveFacets: ['fieldA', 'fieldB'], + facets: { + fieldA: { + size: 30, + type: 'value', + }, + fieldB: { + size: 30, + type: 'value', + }, + }, + result_fields: { + bar: { + raw: {}, + snippet: { + fallback: true, + size: 300, + }, + }, + foo: { + raw: {}, + snippet: { + fallback: true, + size: 300, + }, + }, }, }, + trackUrlState: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 78e1fa9e7f3a2..ac10e2db7191e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -5,8 +5,17 @@ */ import { Schema } from '../../../../shared/types'; +import { Fields } from './types'; + +export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => { + const facets = fields.filterFields.reduce( + (facetsConfig, fieldName) => ({ + ...facetsConfig, + [fieldName]: { type: 'value', size: 30 }, + }), + {} + ); -export const buildSearchUIConfig = (apiConnector: object, schema: Schema) => { return { alwaysSearchOnInitialLoad: true, apiConnector, @@ -16,6 +25,8 @@ export const buildSearchUIConfig = (apiConnector: object, schema: Schema) => { sortField: 'id', }, searchQuery: { + disjunctiveFacets: fields.filterFields, + facets, result_fields: Object.keys(schema).reduce((acc: { [key: string]: object }, key: string) => { acc[key] = { snippet: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.ts new file mode 100644 index 0000000000000..e95f91f6f6f89 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildSortOptions } from './build_sort_options'; + +describe('buildSortOptions', () => { + it('builds sort options from a list of field names', () => { + const sortOptions = buildSortOptions( + { + filterFields: [], + sortFields: ['fieldA', 'fieldB'], + }, + [ + { + name: 'Relevance (asc)', + value: 'id', + direction: 'desc', + }, + { + name: 'Relevance (desc)', + value: 'id', + direction: 'asc', + }, + ] + ); + + expect(sortOptions).toEqual([ + { + name: 'Relevance (asc)', + value: 'id', + direction: 'desc', + }, + { + name: 'Relevance (desc)', + value: 'id', + direction: 'asc', + }, + { + direction: 'asc', + name: 'fieldA (asc)', + value: 'fieldA', + }, + { + direction: 'desc', + name: 'fieldA (desc)', + value: 'fieldA', + }, + { + direction: 'asc', + name: 'fieldB (asc)', + value: 'fieldB', + }, + { + direction: 'desc', + name: 'fieldB (desc)', + value: 'fieldB', + }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts new file mode 100644 index 0000000000000..8b80e557e4b60 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { flatten } from 'lodash'; + +import { Fields, SortOption, SortDirection } from './types'; +import { ASCENDING, DESCENDING } from './constants'; + +const fieldNameToSortOptions = (fieldName: string): SortOption[] => + ['asc', 'desc'].map((direction) => ({ + name: direction === 'asc' ? ASCENDING(fieldName) : DESCENDING(fieldName), + value: fieldName, + direction: direction as SortDirection, + })); + +/** + * Adds two sort options for a given field, a "desc" and an "asc" option. + */ +export const buildSortOptions = ( + fields: Fields, + defaultSortOptions: SortOption[] +): SortOption[] => { + const sortFieldsOptions = flatten(fields.sortFields.map(fieldNameToSortOptions)); + const sortingOptions = [...defaultSortOptions, ...sortFieldsOptions]; + return sortingOptions; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts new file mode 100644 index 0000000000000..1c7c3f5a65bd5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/constants.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ASCENDING = (fieldName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel', + { + defaultMessage: '{fieldName} (asc)', + values: { fieldName }, + } + ); + +export const DESCENDING = (fieldName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel', + { + defaultMessage: '{fieldName} (desc)', + values: { fieldName }, + } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss index ba9931dc90fdc..d2e0a8155fa55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss @@ -25,4 +25,12 @@ background-color: $euiPageBackgroundColor; padding: $euiSizeL; } + + .documentsSearchExperience__facet { + line-height: 0; + + .euiCheckbox__label { + @include euiTextTruncate; + } + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index 5df132a27bbb3..410a4ea5bab7b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -7,25 +7,19 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { setMockValues } from '../../../../__mocks__'; -const mockSetFields = jest.fn(); - jest.mock('../../../../shared/use_local_storage', () => ({ - useLocalStorage: jest.fn(() => [ - { - filterFields: ['a', 'b', 'c'], - sortFields: ['d', 'c'], - }, - mockSetFields, - ]), + useLocalStorage: jest.fn(), })); +import { useLocalStorage } from '../../../../shared/use_local_storage'; import React from 'react'; // @ts-expect-error types are not available for this package yet -import { SearchProvider } from '@elastic/react-search-ui'; -import { shallow } from 'enzyme'; +import { SearchProvider, Facet } from '@elastic/react-search-ui'; +import { shallow, ShallowWrapper } from 'enzyme'; import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; +import { Fields } from './types'; import { SearchExperience } from './search_experience'; @@ -36,8 +30,16 @@ describe('SearchExperience', () => { apiKey: '1234', }, }; + const mockSetFields = jest.fn(); + const setFieldsInLocalStorage = (fields: Fields) => { + (useLocalStorage as jest.Mock).mockImplementation(() => [fields, mockSetFields]); + }; beforeEach(() => { + setFieldsInLocalStorage({ + filterFields: ['a', 'b', 'c'], + sortFields: ['d', 'c'], + }); jest.clearAllMocks(); setMockValues(values); }); @@ -47,12 +49,60 @@ describe('SearchExperience', () => { expect(wrapper.find(SearchProvider).length).toBe(1); }); + describe('when there are no selected filter fields', () => { + let wrapper: ShallowWrapper; + beforeEach(() => { + setFieldsInLocalStorage({ + filterFields: [], + sortFields: ['a', 'b'], + }); + wrapper = shallow(); + }); + + it('shows a customize callout instead of a button if no fields are yet selected', () => { + expect(wrapper.find(CustomizationCallout).exists()).toBe(true); + expect(wrapper.find('[data-test-subj="customize"]').exists()).toBe(false); + }); + + it('will show the customization modal when clicked', () => { + expect(wrapper.find(CustomizationModal).exists()).toBe(false); + wrapper.find(CustomizationCallout).simulate('click'); + + expect(wrapper.find(CustomizationModal).exists()).toBe(true); + }); + }); + + describe('when there are selected filter fields', () => { + let wrapper: ShallowWrapper; + beforeEach(() => { + setFieldsInLocalStorage({ + filterFields: ['a', 'b'], + sortFields: ['a', 'b'], + }); + wrapper = shallow(); + }); + + it('shows a customize button', () => { + expect(wrapper.find(CustomizationCallout).exists()).toBe(false); + expect(wrapper.find('[data-test-subj="customize"]').exists()).toBe(true); + }); + }); + + it('renders Facet components for filter fields', () => { + setFieldsInLocalStorage({ + filterFields: ['a', 'b', 'c'], + sortFields: [], + }); + const wrapper = shallow(); + expect(wrapper.find(Facet).length).toBe(3); + }); + describe('customization modal', () => { it('has a customization modal which can be opened and closed', () => { const wrapper = shallow(); expect(wrapper.find(CustomizationModal).exists()).toBe(false); - wrapper.find(CustomizationCallout).simulate('click'); + wrapper.find('[data-test-subj="customize"]').simulate('click'); expect(wrapper.find(CustomizationModal).exists()).toBe(true); wrapper.find(CustomizationModal).prop('onClose')(); @@ -61,14 +111,14 @@ describe('SearchExperience', () => { it('passes values from localStorage to the customization modal', () => { const wrapper = shallow(); - wrapper.find(CustomizationCallout).simulate('click'); + wrapper.find('[data-test-subj="customize"]').simulate('click'); expect(wrapper.find(CustomizationModal).prop('filterFields')).toEqual(['a', 'b', 'c']); expect(wrapper.find(CustomizationModal).prop('sortFields')).toEqual(['d', 'c']); }); it('updates selected fields in localStorage and closes modal on save', () => { const wrapper = shallow(); - wrapper.find(CustomizationCallout).simulate('click'); + wrapper.find('[data-test-subj="customize"]').simulate('click'); wrapper.find(CustomizationModal).prop('onSave')({ filterFields: ['new', 'filters'], sortFields: ['new', 'sorts'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index e80ab2e18b2d3..d829042bef11f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -7,36 +7,41 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useValues } from 'kea'; -import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet; -import { SearchProvider, SearchBox, Sorting } from '@elastic/react-search-ui'; +import { SearchProvider, SearchBox, Sorting, Facet } from '@elastic/react-search-ui'; // @ts-expect-error types are not available for this package yet import AppSearchAPIConnector from '@elastic/search-ui-app-search-connector'; import './search_experience.scss'; -import { EngineLogic } from '../../engine'; import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; +import { EngineLogic } from '../../engine'; -import { SearchBoxView, SortingView } from './views'; +import { Fields, SortOption } from './types'; +import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; import { SearchExperienceContent } from './search_experience_content'; import { buildSearchUIConfig } from './build_search_ui_config'; import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; +import { buildSortOptions } from './build_sort_options'; +import { ASCENDING, DESCENDING } from './constants'; -const DEFAULT_SORT_OPTIONS = [ +const RECENTLY_UPLOADED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded', + { + defaultMessage: 'Recently Uploaded', + } +); +const DEFAULT_SORT_OPTIONS: SortOption[] = [ { - name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc', { - defaultMessage: 'Recently Uploaded (desc)', - }), + name: DESCENDING(RECENTLY_UPLOADED), value: 'id', direction: 'desc', }, { - name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc', { - defaultMessage: 'Recently Uploaded (asc)', - }), + name: ASCENDING(RECENTLY_UPLOADED), value: 'id', direction: 'asc', }, @@ -50,16 +55,15 @@ export const SearchExperience: React.FC = () => { const openCustomizationModal = () => setShowCustomizationModal(true); const closeCustomizationModal = () => setShowCustomizationModal(false); - const [fields, setFields] = useLocalStorage( + const [fields, setFields] = useLocalStorage( `documents-search-experience-customization--${engine.name}`, { - filterFields: [] as string[], - sortFields: [] as string[], + filterFields: [], + sortFields: [], } ); - // TODO const sortFieldsOptions = _flatten(fields.sortFields.map(fieldNameToSortOptions)) // we need to flatten this array since fieldNameToSortOptions returns an array of two sorting options - const sortingOptions = [...DEFAULT_SORT_OPTIONS /* TODO ...sortFieldsOptions*/]; + const sortingOptions = buildSortOptions(fields, DEFAULT_SORT_OPTIONS); const connector = new AppSearchAPIConnector({ cacheResponses: false, @@ -68,7 +72,7 @@ export const SearchExperience: React.FC = () => { searchKey: engine.apiKey, }); - const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}); + const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}, fields); return (
@@ -101,7 +105,36 @@ export const SearchExperience: React.FC = () => { view={SortingView} /> - + {fields.filterFields.length > 0 ? ( + <> + {fields.filterFields.map((fieldName) => ( +
+ + +
+ ))} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationButton', + { + defaultMessage: 'Customize filters and sort', + } + )} + + + ) : ( + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts new file mode 100644 index 0000000000000..0cde0f94b7738 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/types.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Fields { + filterFields: string[]; + sortFields: string[]; +} + +export type SortDirection = 'asc' | 'desc'; + +export interface SortOption { + name: string; + value: string; + direction: SortDirection; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts index 8c88fc81d3a3c..7032fa1a9a06b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/index.ts @@ -9,3 +9,4 @@ export { SortingView } from './sorting_view'; export { ResultView } from './result_view'; export { ResultsPerPageView } from './results_per_page_view'; export { PagingView } from './paging_view'; +export { MultiCheckboxFacetsView } from './multi_checkbox_facets_view'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx new file mode 100644 index 0000000000000..7f43ca12652c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { MultiCheckboxFacetsView } from './multi_checkbox_facets_view'; + +describe('MultiCheckboxFacetsView', () => { + const props = { + label: 'foo', + options: [ + { + value: 'value1', + selected: false, + }, + { + value: 'value2', + selected: false, + }, + ], + showMore: true, + onMoreClick: jest.fn(), + onRemove: jest.fn(), + onSelect: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('calls onMoreClick when more button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="more"]').simulate('click'); + expect(props.onMoreClick).toHaveBeenCalled(); + }); + + it('calls onSelect when an option is selected', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="checkbox-group"]').simulate('change', 'generated-id_1'); + expect(props.onSelect).toHaveBeenCalledWith('value2'); + }); + + it('calls onRemove if the option was already selected', () => { + const wrapper = shallow( + + ); + wrapper.find('[data-test-subj="checkbox-group"]').simulate('change', 'generated-id_1'); + expect(props.onRemove).toHaveBeenCalledWith('value2'); + }); + + it('it passes options to EuiCheckboxGroup, converting no values to the text "No Value"', () => { + const wrapper = shallow( + + ); + const options = wrapper.find('[data-test-subj="checkbox-group"]').prop('options'); + expect(options).toEqual([ + { id: 'generated-id_0', label: 'value1' }, + { id: 'generated-id_1', label: '' }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx new file mode 100644 index 0000000000000..df61e6e3dcc05 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/multi_checkbox_facets_view.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { + htmlIdGenerator, + EuiCheckboxGroup, + EuiFlexGroup, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Option { + value: string; + selected: boolean; +} + +interface Props { + label: string; + options: Option[]; + showMore: boolean; + onMoreClick(): void; + onRemove(id: string): void; + onSelect(id: string): void; +} + +const getIndexFromId = (id: string) => parseInt(id.split('_')[1], 10); + +export const MultiCheckboxFacetsView: React.FC = ({ + label, + onMoreClick, + onRemove, + onSelect, + options, + showMore, +}) => { + const getId = htmlIdGenerator(); + + const optionToCheckBoxGroupOption = (option: Option, index: number) => ({ + id: getId(String(index)), + label: + option.value || + i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.multiCheckboxFacetsView.noValue.selectOption', + { + defaultMessage: '', + } + ), + }); + + const optionToSelectedMapReducer = ( + selectedMap: { [name: string]: boolean }, + option: Option, + index: number + ) => { + if (option.selected) { + selectedMap[getId(String(index))] = true; + } + return selectedMap; + }; + + const checkboxGroupOptions = options.map(optionToCheckBoxGroupOption); + const idToSelectedMap = options.reduce(optionToSelectedMapReducer, {}); + + const onChange = (checkboxId: string) => { + const index = getIndexFromId(checkboxId); + const option = options[index]; + if (option.selected) { + onRemove(option.value); + return; + } + onSelect(option.value); + }; + + return ( + <> + + {showMore && ( + <> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.multiCheckboxFacetsView.showMore', + { + defaultMessage: 'Show more', + } + )} + + + + )} + + ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 07befe8a26b2f..1bbf4b8033755 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7208,8 +7208,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.indexingGuide": "インデックスガイドをお読みください", "xpack.enterpriseSearch.appSearch.documents.search.noResults": "「{resultSearchTerm}」の結果がありません。", "xpack.enterpriseSearch.appSearch.documents.search.placeholder": "ドキュメントのフィルター...", - "xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc": "最近アップロードされたドキュメント(昇順)", - "xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc": "最近アップロードされたドキュメント(降順)", "xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.ariaLabel": "1 ページに表示する結果数", "xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.show": "表示:", "xpack.enterpriseSearch.appSearch.documents.search.sortBy": "並べ替え基準", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 87af04f7dec87..51205a3420be5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7227,8 +7227,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.indexingGuide": "请阅读索引指南", "xpack.enterpriseSearch.appSearch.documents.search.noResults": "还没有匹配“{resultSearchTerm}”的结果!", "xpack.enterpriseSearch.appSearch.documents.search.placeholder": "筛选文档......", - "xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc": "最近上传(升序)", - "xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc": "最近上传(降序)", "xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.ariaLabel": "每页要显示的结果数", "xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.show": "显示:", "xpack.enterpriseSearch.appSearch.documents.search.sortBy": "排序依据", From 1edc7998946481358870c8bc881a604d8ba169db Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 20 Jan 2021 14:21:38 -0600 Subject: [PATCH 20/28] [index patterns] improve developer docs (#86416) * add index pattern docs --- ...blic.iindexpattern.getformatterforfield.md | 2 + ...lugin-plugins-data-public.iindexpattern.md | 6 +- ...-plugins-data-public.iindexpattern.type.md | 2 + ...ins-data-public.indexpattern.fieldattrs.md | 11 --- ...s-data-public.indexpattern.intervalname.md | 5 + ...plugin-plugins-data-public.indexpattern.md | 9 +- ...plugins-data-public.indexpattern.tospec.md | 2 + ...n-plugins-data-public.indexpattern.type.md | 2 + ...ugins-data-public.indexpattern.typemeta.md | 2 + ...lugins-data-public.indexpattern.version.md | 2 + ...gins-data-public.indexpatternattributes.md | 2 + ...plugins-data-public.indexpatternspec.id.md | 2 + ...ta-public.indexpatternspec.intervalname.md | 5 + ...in-plugins-data-public.indexpatternspec.md | 6 +- ...ns-data-public.indexpatternspec.version.md | 2 + ...data-public.indexpatternsservice.create.md | 2 + ...s-data-public.indexpatternsservice.find.md | 2 + ...lugins-data-public.indexpatternsservice.md | 2 +- .../kibana-plugin-plugins-data-public.md | 6 +- ...ins-data-server.indexpattern.fieldattrs.md | 11 --- ...s-data-server.indexpattern.intervalname.md | 5 + ...plugin-plugins-data-server.indexpattern.md | 9 +- ...plugins-data-server.indexpattern.tospec.md | 2 + ...n-plugins-data-server.indexpattern.type.md | 2 + ...ugins-data-server.indexpattern.typemeta.md | 2 + ...lugins-data-server.indexpattern.version.md | 2 + ...gins-data-server.indexpatternattributes.md | 2 + .../kibana-plugin-plugins-data-server.md | 2 +- src/plugins/data/README.mdx | 92 +++++++++++++++++-- .../index_patterns/index_pattern.ts | 20 +++- .../index_patterns/index_patterns.ts | 15 ++- .../data/common/index_patterns/types.ts | 44 +++++++++ src/plugins/data/public/public.api.md | 29 ++---- src/plugins/data/server/server.api.md | 20 +--- 34 files changed, 240 insertions(+), 89 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md index 7466e4b9cf658..5fc29ca5031b4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md @@ -4,6 +4,8 @@ ## IIndexPattern.getFormatterForField property +Look up a formatter for a given field + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index ba77e659f0834..3a78395b42754 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -4,6 +4,8 @@ ## IIndexPattern interface +IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided + Signature: ```typescript @@ -16,11 +18,11 @@ export interface IIndexPattern | --- | --- | --- | | [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, SerializedFieldFormat<unknown> | undefined> | | | [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[] | | -| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | | +| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | Look up a formatter for a given field | | [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | -| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | | +| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | Type is used for identifying rollup indices, otherwise left undefined | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.type.md index ea75c20b403c0..d517163090c85 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.type.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.type.md @@ -4,6 +4,8 @@ ## IIndexPattern.type property +Type is used for identifying rollup indices, otherwise left undefined + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md deleted file mode 100644 index c2e0b9bb855f4..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) - -## IndexPattern.fieldAttrs property - -Signature: - -```typescript -fieldAttrs: FieldAttrs; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md index 762b4a37bfd28..81e7fb9c1d57b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md @@ -4,6 +4,11 @@ ## IndexPattern.intervalName property +> Warning: This API is now obsolete. +> +> Deprecated. used by time range index patterns +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index b640ef1b89606..872e23e450f88 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -22,7 +22,6 @@ export declare class IndexPattern implements IIndexPattern | --- | --- | --- | --- | | [allowNoIndex](./kibana-plugin-plugins-data-public.indexpattern.allownoindex.md) | | boolean | prevents errors when index pattern exists before indices | | [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | -| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | @@ -38,9 +37,9 @@ export declare class IndexPattern implements IIndexPattern | [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | -| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | undefined | | -| [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | | -| [version](./kibana-plugin-plugins-data-public.indexpattern.version.md) | | string | undefined | | +| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | undefined | Type is used to identify rollup index patterns | +| [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | Only used by rollup indices, used by rollup specific endpoint to load field list | +| [version](./kibana-plugin-plugins-data-public.indexpattern.version.md) | | string | undefined | SavedObject version | ## Methods @@ -63,5 +62,5 @@ export declare class IndexPattern implements IIndexPattern | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md) | | | -| [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | +| [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | Create static representation of index pattern | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md index d1a78eea660ce..d8153530e5c13 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md @@ -4,6 +4,8 @@ ## IndexPattern.toSpec() method +Create static representation of index pattern + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md index 7a10d058b9c65..0f9572d1bad24 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md @@ -4,6 +4,8 @@ ## IndexPattern.type property +Type is used to identify rollup index patterns + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.typemeta.md index ea8533a8d837c..ce316ff9638ac 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.typemeta.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.typemeta.md @@ -4,6 +4,8 @@ ## IndexPattern.typeMeta property +Only used by rollup indices, used by rollup specific endpoint to load field list + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md index 99d3bc4e7a04d..2083bd65e9b0a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md @@ -4,6 +4,8 @@ ## IndexPattern.version property +SavedObject version + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 1bbede5658942..297bfa855f0eb 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -4,6 +4,8 @@ ## IndexPatternAttributes interface +Interface for an index pattern saved object + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md index 55eadbf36c660..807f777841685 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md @@ -4,6 +4,8 @@ ## IndexPatternSpec.id property +saved object id + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md index 98748661256da..90c5ee5666231 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md @@ -4,6 +4,11 @@ ## IndexPatternSpec.intervalName property +> Warning: This API is now obsolete. +> +> Deprecated. Was used by time range based index patterns +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index 9357ad7d5077e..c0fa165cfb115 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -4,6 +4,8 @@ ## IndexPatternSpec interface +Static index pattern format Serialized data object, representing index pattern attributes and state + Signature: ```typescript @@ -18,12 +20,12 @@ export interface IndexPatternSpec | [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md) | FieldAttrs | | | [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) | Record<string, SerializedFieldFormat> | | | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | -| [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | +| [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | saved object id | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.indexpatternspec.type.md) | string | | | [typeMeta](./kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md) | TypeMeta | | -| [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) | string | | +| [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) | string | saved object version string | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md index 43f7cf0226fb0..60975b94e9633 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md @@ -4,6 +4,8 @@ ## IndexPatternSpec.version property +saved object version string + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md index d7152ba617cc6..c8e845eb1d1bf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md @@ -23,3 +23,5 @@ create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise `Promise` +IndexPattern + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.find.md index f642965c5da80..929322fc4794c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.find.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.find.md @@ -4,6 +4,8 @@ ## IndexPatternsService.find property +Find and load index patterns by title + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 30ce1fa1de386..1511de18cab51 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -23,7 +23,7 @@ export declare class IndexPatternsService | [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | | [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | | [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | -| [find](./kibana-plugin-plugins-data-public.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | | +| [find](./kibana-plugin-plugins-data-public.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6a3e7662e59bc..65a722868b37f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -65,12 +65,12 @@ | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | | +| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided | | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | | -| [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Interface for an index pattern saved object | +| [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | Static index pattern format Serialized data object, representing index pattern attributes and state | | [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md deleted file mode 100644 index c8bad55dee2e4..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) - -## IndexPattern.fieldAttrs property - -Signature: - -```typescript -fieldAttrs: FieldAttrs; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md index caaa6929235f8..c144520075790 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md @@ -4,6 +4,11 @@ ## IndexPattern.intervalName property +> Warning: This API is now obsolete. +> +> Deprecated. used by time range index patterns +> + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 54f020e57cf4a..70c37ba1b3926 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -22,7 +22,6 @@ export declare class IndexPattern implements IIndexPattern | --- | --- | --- | --- | | [allowNoIndex](./kibana-plugin-plugins-data-server.indexpattern.allownoindex.md) | | boolean | prevents errors when index pattern exists before indices | | [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | -| [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | @@ -38,9 +37,9 @@ export declare class IndexPattern implements IIndexPattern | [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string | | -| [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) | | string | undefined | | -| [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) | | TypeMeta | | -| [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) | | string | undefined | | +| [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) | | string | undefined | Type is used to identify rollup index patterns | +| [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) | | TypeMeta | Only used by rollup indices, used by rollup specific endpoint to load field list | +| [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) | | string | undefined | SavedObject version | ## Methods @@ -63,5 +62,5 @@ export declare class IndexPattern implements IIndexPattern | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md) | | | -| [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | | +| [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | Create static representation of index pattern | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md index 5d76b8f00853b..7c3c392cf6df3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md @@ -4,6 +4,8 @@ ## IndexPattern.toSpec() method +Create static representation of index pattern + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md index 01154ab5444d1..cc64e413ef4c8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md @@ -4,6 +4,8 @@ ## IndexPattern.type property +Type is used to identify rollup index patterns + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md index b16bcec404d97..b759900a186ca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md @@ -4,6 +4,8 @@ ## IndexPattern.typeMeta property +Only used by rollup indices, used by rollup specific endpoint to load field list + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md index e4297d8389111..583a0c5ab6c5b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md @@ -4,6 +4,8 @@ ## IndexPattern.version property +SavedObject version + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index b9b9f955c7ab5..bfc7f65425f9c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -4,6 +4,8 @@ ## IndexPatternAttributes interface +Interface for an index pattern saved object + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 1b4cf5585cb3e..e6cb5accb9e31 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -51,7 +51,7 @@ | [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Interface for an index pattern saved object | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 2448d5f22ced2..145aaa64fa3ad 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -20,7 +20,7 @@ It is wired into the `TopNavMenu` component, but can be used independently. ### Fetch Query Suggestions -The `getQuerySuggestions` function helps to construct a query. +The `getQuerySuggestions` function helps to construct a query. KQL suggestion functions are registered in X-Pack, so this API does not return results in OSS. ```.ts @@ -37,7 +37,7 @@ KQL suggestion functions are registered in X-Pack, so this API does not return r ### Fetch Value Suggestions The `getValueSuggestions` function returns suggestions for field values. -This is helpful when you want to provide a user with options, for example when constructing a filter. +This is helpful when you want to provide a user with options, for example when constructing a filter. ```.ts @@ -56,7 +56,81 @@ Coming soon. ## Index Patterns -Coming soon. +The Index Patterns API provides a consistent method of structuring and formatting documents +and field lists across the various Kibana apps. Its typically used in conjunction with +SearchSource for composing queries. + +### Index Patterns API + +- Get list of index patterns +- Get default index pattern and examine fields +- Get index pattern by id +- Find index pattern by title +- Create index pattern +- Create index pattern and save it +- Modify index pattern and save it +- Delete index pattern + +#### Get list of index pattern titles and ids + +``` +const idsAndTitles = await data.indexPatterns.getIdsWithTitle(); +idsAndTitles.forEach(({id, title}) => console.log(`Index pattern id: ${id} title: ${title}`)); +``` + +#### Get default index pattern and examine fields + +``` +const defaultIndexPattern = await data.indexPatterns.getDefault(); +defaultIndexPattern.fields.forEach(({name}) => { console.log(name); }) +``` + +#### Get index pattern by id + +``` +const id = 'xxxxxx-xxx-xxxxxx'; +const indexPattern = await data.indexPatterns.get(id); +``` + +#### Find index pattern by title + +``` +const title = 'kibana-*'; +const [indexPattern] = await data.indexPatterns.find(title); +``` + +#### Create index pattern + +``` +const indexPattern = await data.indexPatterns.create({ title: 'kibana-*' }); +``` + +#### Create index pattern and save it immediately + +``` +const indexPattern = await data.indexPatterns.createAndSave({ title: 'kibana-*' }); +``` + +#### Create index pattern, modify, and save + +``` +const indexPattern = await data.indexPatterns.create({ title: 'kibana-*' }); +indexPattern.setFieldCustomLabel('customer_name', 'Customer Name'); +data.indexPatterns.createSavedObject(indexPattern); +``` + +#### Modify index pattern and save it + +``` +indexPattern.setFieldCustomLabel('customer_name', 'Customer Name'); +await data.indexPatterns.updateSavedObject(indexPattern); +``` + +#### Delete index pattern + +``` +await data.indexPatterns.delete(indexPatternId); +``` ### Index Patterns HTTP API @@ -79,7 +153,7 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints: ### Index Patterns API -Index Patterns CURD API allows you to create, retrieve and delete index patterns. I also +Index Patterns REST API allows you to create, retrieve and delete index patterns. I also exposes an update endpoint which allows you to update specific fields without changing the rest of the index pattern object. @@ -146,7 +220,7 @@ The endpoint returns the created index pattern object. #### Fetch an index pattern by ID -Retrieve and index pattern by its ID. +Retrieve an index pattern by its ID. ``` GET /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx @@ -477,7 +551,7 @@ It contains sub-services for each of those configurations: // Constuct the query portion of the search request const query = data.query.getEsQuery(indexPattern); - + // Construct a request const request = { params: { @@ -522,7 +596,7 @@ The `SearchSource` API is a convenient way to construct and run an Elasticsearch #### Default Search Strategy One benefit of using the low-level search API, is partial response support in X-Pack, allowing for a better and more responsive user experience. -In OSS only the final result is returned. +In OSS only the final result is returned. ```.ts import { isCompleteResponse } from '../plugins/data/public'; @@ -538,8 +612,8 @@ In OSS only the final result is returned. } }, error: (e: Error) => { - // Show customized toast notifications. - // You may choose to handle errors differently if you prefer. + // Show customized toast notifications. + // You may choose to handle errors differently if you prefer. data.search.showError(e); }, }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index ec665a80cbe0b..452c663d96716 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -43,10 +43,20 @@ export class IndexPattern implements IIndexPattern { public id?: string; public title: string = ''; public fieldFormatMap: Record; + /** + * Only used by rollup indices, used by rollup specific endpoint to load field list + */ public typeMeta?: TypeMeta; public fields: IIndexPatternFieldList & { toSpec: () => IndexPatternFieldMap }; public timeFieldName: string | undefined; + /** + * @deprecated + * Deprecated. used by time range index patterns + */ public intervalName: string | undefined; + /** + * Type is used to identify rollup index patterns + */ public type: string | undefined; public formatHit: { (hit: Record, type?: string): any; @@ -55,14 +65,15 @@ export class IndexPattern implements IIndexPattern { public formatField: FormatFieldFn; public flattenHit: (hit: Record, deep?: boolean) => Record; public metaFields: string[]; - // savedObject version + /** + * SavedObject version + */ public version: string | undefined; public sourceFilters?: SourceFilter[]; private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; - // make private once manual field refresh is removed - public fieldAttrs: FieldAttrs; + private fieldAttrs: FieldAttrs; /** * prevents errors when index pattern exists before indices */ @@ -184,6 +195,9 @@ export class IndexPattern implements IIndexPattern { }; } + /** + * Create static representation of index pattern + */ public toSpec(): IndexPatternSpec { return { id: this.id, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 4d5e666a70c0d..80cb8a55fa0a0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -127,6 +127,12 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.attributes?.title); }; + /** + * Find and load index patterns by title + * @param search + * @param size + * @returns IndexPattern[] + */ find = async (search: string, size: number = 10): Promise => { const savedObjects = await this.savedObjectsClient.find({ type: 'index-pattern', @@ -206,6 +212,7 @@ export class IndexPatternsService { /** * Get field list by providing { pattern } * @param options + * @returns FieldSpec[] */ getFieldsForWildcard = async (options: GetFieldsOptions) => { const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); @@ -221,6 +228,7 @@ export class IndexPatternsService { /** * Get field list by providing an index patttern (or spec) * @param options + * @returns FieldSpec[] */ getFieldsForIndexPattern = async ( indexPattern: IndexPattern | IndexPatternSpec, @@ -266,6 +274,7 @@ export class IndexPatternsService { * @param id * @param title * @param options + * @returns Record */ private refreshFieldSpecMap = async ( fields: IndexPatternFieldMap, @@ -307,7 +316,9 @@ export class IndexPatternsService { /** * Converts field array to map - * @param fields + * @param fields: FieldSpec[] + * @param fieldAttrs: FieldAttrs + * @returns Record */ fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { @@ -322,6 +333,7 @@ export class IndexPatternsService { /** * Converts index pattern saved object to index pattern spec * @param savedObject + * @returns IndexPatternSpec */ savedObjectToSpec = (savedObject: SavedObject): IndexPatternSpec => { @@ -442,6 +454,7 @@ export class IndexPatternsService { * Create a new index pattern instance * @param spec * @param skipFetchFields + * @returns IndexPattern */ async create(spec: IndexPatternSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 77c251ada7b21..9f9a26604a0e5 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -15,19 +15,32 @@ import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; +/** + * IIndexPattern allows for an IndexPattern OR an index pattern saved object + * too ambiguous, should be avoided + */ export interface IIndexPattern { fields: IFieldType[]; title: string; id?: string; + /** + * Type is used for identifying rollup indices, otherwise left undefined + */ type?: string; timeFieldName?: string; getTimeField?(): IFieldType | undefined; fieldFormatMap?: Record | undefined>; + /** + * Look up a formatter for a given field + */ getFormatterForField?: ( field: IndexPatternField | IndexPatternField['spec'] | IFieldType ) => FieldFormat; } +/** + * Interface for an index pattern saved object + */ export interface IndexPatternAttributes { type: string; fields: string; @@ -44,6 +57,10 @@ export interface IndexPatternAttributes { allowNoIndex?: boolean; } +/** + * @intenal + * Storage of field attributes. Necessary since the field list isn't saved. + */ export interface FieldAttrs { [key: string]: FieldAttrSet; } @@ -153,9 +170,22 @@ export interface FieldSpecExportFmt { indexed?: boolean; } +/** + * Serialized version of IndexPatternField + */ export interface FieldSpec { + /** + * Popularity count is used by discover + */ count?: number; + /** + * Scripted field painless script + */ script?: string; + /** + * Scripted field langauge + * Painless is the only valid scripted field language + */ lang?: string; conflictDescriptions?: Record; format?: SerializedFieldFormat; @@ -175,10 +205,24 @@ export interface FieldSpec { export type IndexPatternFieldMap = Record; +/** + * Static index pattern format + * Serialized data object, representing index pattern attributes and state + */ export interface IndexPatternSpec { + /** + * saved object id + */ id?: string; + /** + * saved object version string + */ version?: string; title?: string; + /** + * @deprecated + * Deprecated. Was used by time range based index patterns + */ intervalName?: string; timeFieldName?: string; sourceFilters?: SourceFilter[]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index e521e468d14a4..34b4dc9116302 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1176,7 +1176,7 @@ export interface IFieldType { // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface IIndexPattern { // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // @@ -1184,7 +1184,6 @@ export interface IIndexPattern { fieldFormatMap?: Record | undefined>; // (undocumented) fields: IFieldType[]; - // (undocumented) getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; // (undocumented) getTimeField?(): IFieldType | undefined; @@ -1194,7 +1193,6 @@ export interface IIndexPattern { timeFieldName?: string; // (undocumented) title: string; - // (undocumented) type?: string; } @@ -1263,10 +1261,6 @@ export class IndexPattern implements IIndexPattern { readonly allowNoIndex: boolean; // (undocumented) readonly deleteFieldFormat: (fieldName: string) => void; - // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts - // - // (undocumented) - fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // (undocumented) @@ -1342,7 +1336,7 @@ export class IndexPattern implements IIndexPattern { getTimeField(): IndexPatternField | undefined; // (undocumented) id?: string; - // (undocumented) + // @deprecated (undocumented) intervalName: string | undefined; // (undocumented) isTimeBased(): boolean; @@ -1368,13 +1362,9 @@ export class IndexPattern implements IIndexPattern { timeFieldName: string | undefined; // (undocumented) title: string; - // (undocumented) toSpec(): IndexPatternSpec; - // (undocumented) type: string | undefined; - // (undocumented) typeMeta?: IndexPatternTypeMeta; - // (undocumented) version: string | undefined; } @@ -1392,7 +1382,7 @@ export type IndexPatternAggRestrictions = Record, 'isLo // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface IndexPatternSpec { // (undocumented) allowNoIndex?: boolean; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + // // (undocumented) fieldAttrs?: FieldAttrs; // (undocumented) fieldFormats?: Record; // (undocumented) fields?: IndexPatternFieldMap; - // (undocumented) id?: string; - // (undocumented) + // @deprecated (undocumented) intervalName?: string; // (undocumented) sourceFilters?: SourceFilter[]; @@ -1547,7 +1538,6 @@ export interface IndexPatternSpec { type?: string; // (undocumented) typeMeta?: IndexPatternTypeMeta; - // (undocumented) version?: string; } @@ -1567,7 +1557,6 @@ export class IndexPatternsService { // (undocumented) ensureDefaultIndexPattern: EnsureDefaultIndexPattern; fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; - // (undocumented) find: (search: string, size?: number) => Promise; get: (id: string) => Promise; // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts @@ -2583,8 +2572,8 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:53:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:122:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/search_source/search_source.ts:186:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ccb40c1ea4b80..15594dc80c888 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -691,10 +691,6 @@ export class IndexPattern implements IIndexPattern { readonly allowNoIndex: boolean; // (undocumented) readonly deleteFieldFormat: (fieldName: string) => void; - // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts - // - // (undocumented) - fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts @@ -774,7 +770,7 @@ export class IndexPattern implements IIndexPattern { getTimeField(): IndexPatternField | undefined; // (undocumented) id?: string; - // (undocumented) + // @deprecated (undocumented) intervalName: string | undefined; // (undocumented) isTimeBased(): boolean; @@ -803,22 +799,16 @@ export class IndexPattern implements IIndexPattern { // (undocumented) title: string; // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) toSpec(): IndexPatternSpec; - // (undocumented) type: string | undefined; // Warning: (ae-forgotten-export) The symbol "TypeMeta" needs to be exported by the entry point index.d.ts - // - // (undocumented) typeMeta?: TypeMeta; - // (undocumented) version: string | undefined; } // Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface IndexPatternAttributes { allowNoIndex?: boolean; // (undocumented) @@ -1390,9 +1380,9 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:47:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:53:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:122:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:50:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:46:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts From 954c8870069252dfcea77265aeecbf360652a8a4 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Wed, 20 Jan 2021 15:25:45 -0500 Subject: [PATCH 21/28] [Uptime] waterfall add fallback support for uncommon mime types (#88691) * uptime waterfall add fallback support for uncommon mime types * update data_formatting test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../waterfall/data_formatting.test.ts | 46 ++++++++++++++++++- .../step_detail/waterfall/data_formatting.ts | 2 +- .../synthetics/step_detail/waterfall/types.ts | 2 + 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index a58927dfbd12f..5c0b36874004a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,7 +5,8 @@ */ import { colourPalette, getSeriesAndDomain } from './data_formatting'; -import { NetworkItems } from './types'; +import { NetworkItems, MimeType } from './types'; +import { WaterfallDataEntry } from '../../waterfall/types'; describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { @@ -136,6 +137,31 @@ describe('getSeriesAndDomain', () => { }, ]; + const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, + ]; + it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -456,4 +482,22 @@ describe('getSeriesAndDomain', () => { } `); }); + + it('handles formatting when mime type is not mapped to a specific mime type bucket', () => { + const actual = getSeriesAndDomain(networkItemsWithUncommonMimeType); + const { series } = actual; + /* verify that raw mime type appears in the tooltip config and that + * the colour is mapped to mime type other */ + const contentDownloadedingConfigItem = series.find((item: WaterfallDataEntry) => { + const { tooltipProps } = item.config; + if (tooltipProps && typeof tooltipProps.value === 'string') { + return ( + tooltipProps.value.includes('application/x-javascript') && + tooltipProps.colour === colourPalette[MimeType.Other] + ); + } + return false; + }); + expect(contentDownloadedingConfigItem).toBeDefined(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 43fa93fa5f6f2..5e59026fd65f8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -50,7 +50,7 @@ const getFriendlyTooltipValue = ({ let label = FriendlyTimingLabels[timing]; if (timing === Timings.Receive && mimeType) { const formattedMimeType: MimeType = MimeTypesMap[mimeType]; - label += ` (${FriendlyMimetypeLabels[formattedMimeType]})`; + label += ` (${FriendlyMimetypeLabels[formattedMimeType] || mimeType})`; } return `${label}: ${formatValueForDisplay(value)}ms`; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 738929741ddaf..137c0767a83e6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -111,6 +111,7 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, + 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -130,6 +131,7 @@ export const MimeTypesMap: Record = { 'audio/x-pn-wav': MimeType.Media, 'audio/webm': MimeType.Media, 'video/webm': MimeType.Media, + 'video/mp4': MimeType.Media, 'audio/ogg': MimeType.Media, 'video/ogg': MimeType.Media, 'application/ogg': MimeType.Media, From da8abdaf754e954b442ed7b81cd3e2fa62a0bf2b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 20 Jan 2021 14:29:47 -0700 Subject: [PATCH 22/28] [Maps] fix tags changed in Maps Save dialog don't refresh until the map is reopened (#88849) * [Maps] fix tags changed in Maps Save dialog don't refresh until the map is reopened * only set tags if newTags are provided --- .../maps/public/routes/map_page/saved_map/saved_map.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index f3c3ec528c034..27fd78980710f 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -292,8 +292,12 @@ export class SavedMap { const prevTitle = this._attributes.title; const prevDescription = this._attributes.description; + const prevTags = this._tags; this._attributes.title = newTitle; this._attributes.description = newDescription; + if (newTags) { + this._tags = newTags; + } this._syncAttributesWithStore(); let updatedMapEmbeddableInput: MapEmbeddableInput; @@ -316,6 +320,7 @@ export class SavedMap { // Error toast displayed by wrapAttributes this._attributes.title = prevTitle; this._attributes.description = prevDescription; + this._tags = prevTags; return; } From 82c1501924dd0cb3d11ef5fbc2921498d48082a1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 20 Jan 2021 16:55:29 -0500 Subject: [PATCH 23/28] [CI] [TeamCity] Move PR commit status publishing gate to accommodate PR bot (#88911) --- .teamcity/src/builds/PullRequestCi.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.teamcity/src/builds/PullRequestCi.kt b/.teamcity/src/builds/PullRequestCi.kt index c38591fe850ac..997cf1771cc8d 100644 --- a/.teamcity/src/builds/PullRequestCi.kt +++ b/.teamcity/src/builds/PullRequestCi.kt @@ -63,13 +63,15 @@ object PullRequestCi : BuildType({ } features { - commitStatusPublisher { - enabled = isReportingEnabled() - vcsRootExtId = "${Kibana.id}" - publisher = github { - githubUrl = "https://api.github.com" - authType = personalToken { - token = "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b" + if(isReportingEnabled()) { + commitStatusPublisher { + enabled = true + vcsRootExtId = "${Kibana.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b" + } } } } From 25f16db4d9791a8943b386cccb8680d70bc6bf54 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 20 Jan 2021 17:39:21 -0500 Subject: [PATCH 24/28] Sharing saved objects, phase 2 (#80945) --- docs/api/saved-objects.asciidoc | 3 + docs/api/saved-objects/get.asciidoc | 2 +- docs/api/saved-objects/resolve.asciidoc | 130 ++ ...public.savedobject.coremigrationversion.md | 13 + .../kibana-plugin-core-public.savedobject.md | 1 + ...jectscreateoptions.coremigrationversion.md | 13 + ...n-core-public.savedobjectscreateoptions.md | 1 + ...-public.simplesavedobject._constructor_.md | 4 +- ....simplesavedobject.coremigrationversion.md | 11 + ...na-plugin-core-public.simplesavedobject.md | 3 +- .../core/server/kibana-plugin-core-server.md | 4 +- ...server.savedobject.coremigrationversion.md | 13 + .../kibana-plugin-core-server.savedobject.md | 1 + ...gin-core-server.savedobjectmigrationmap.md | 2 +- ...tsbulkcreateobject.coremigrationversion.md | 18 + ...ore-server.savedobjectsbulkcreateobject.md | 1 + ...a-plugin-core-server.savedobjectsclient.md | 1 + ...-core-server.savedobjectsclient.resolve.md | 26 + ...jectscreateoptions.coremigrationversion.md | 18 + ...n-core-server.savedobjectscreateoptions.md | 1 + ...e-server.savedobjectsrawdocparseoptions.md | 20 + ...tsrawdocparseoptions.namespacetreatment.md | 15 + ...ugin-core-server.savedobjectsrepository.md | 1 + ...e-server.savedobjectsrepository.resolve.md | 28 + ...core-server.savedobjectsresolveresponse.md | 20 + ...ver.savedobjectsresolveresponse.outcome.md | 15 + ...avedobjectsresolveresponse.saved_object.md | 11 + ...sserializer.generaterawlegacyurlaliasid.md | 26 + ...savedobjectsserializer.israwsavedobject.md | 5 +- ...ugin-core-server.savedobjectsserializer.md | 5 +- ...savedobjectsserializer.rawtosavedobject.md | 3 +- ...type.converttomultinamespacetypeversion.md | 42 + ...ana-plugin-core-server.savedobjectstype.md | 22 + ...plugin-plugins-data-server.plugin.start.md | 4 +- docs/user/security/audit-logging.asciidoc | 4 + src/core/public/public.api.md | 6 +- .../saved_objects/saved_objects_client.ts | 2 + .../saved_objects/simple_saved_object.ts | 14 +- .../core_usage_stats_client.mock.ts | 1 + .../core_usage_stats_client.test.ts | 76 + .../core_usage_stats_client.ts | 6 + src/core/server/core_usage_data/types.ts | 7 + src/core/server/index.ts | 2 + src/core/server/saved_objects/index.ts | 1 + .../migrations/core/__mocks__/index.ts | 13 + .../build_active_mappings.test.ts.snap | 8 + .../migrations/core/build_active_mappings.ts | 3 + .../migrations/core/document_migrator.test.ts | 1665 +++++++++++------ .../migrations/core/document_migrator.ts | 551 +++++- .../migrations/core/elastic_index.test.ts | 21 +- .../migrations/core/elastic_index.ts | 32 +- .../migrations/core/index_migrator.test.ts | 27 +- .../migrations/core/index_migrator.ts | 7 +- .../migrations/core/migrate_raw_docs.test.ts | 84 +- .../migrations/core/migrate_raw_docs.ts | 23 +- .../migrations/core/migration_context.ts | 3 + .../kibana_migrator.test.ts.snap | 4 + .../migrations/kibana/kibana_migrator.ts | 4 +- .../server/saved_objects/migrations/types.ts | 2 +- .../saved_objects/object_types/constants.ts | 12 + .../saved_objects/object_types/index.ts | 11 + .../object_types/registration.test.ts | 25 + .../object_types/registration.ts | 29 + .../saved_objects/object_types/types.ts | 19 + .../saved_objects/routes/bulk_create.ts | 1 + .../server/saved_objects/routes/create.ts | 18 +- src/core/server/saved_objects/routes/index.ts | 2 + .../routes/integration_tests/resolve.test.ts | 91 + .../server/saved_objects/routes/resolve.ts | 38 + .../saved_objects_service.test.ts | 13 + .../saved_objects/saved_objects_service.ts | 3 + .../saved_objects/serialization/index.ts | 1 + .../serialization/serializer.test.ts | 215 +++ .../saved_objects/serialization/serializer.ts | 132 +- .../saved_objects/serialization/types.ts | 17 + .../service/lib/included_fields.test.ts | 4 +- .../service/lib/included_fields.ts | 1 + .../service/lib/repository.mock.ts | 1 + .../service/lib/repository.test.js | 235 ++- .../saved_objects/service/lib/repository.ts | 191 +- .../service/saved_objects_client.mock.ts | 1 + .../service/saved_objects_client.test.js | 16 + .../service/saved_objects_client.ts | 53 + src/core/server/saved_objects/types.ts | 35 + src/core/server/server.api.md | 37 +- src/core/types/saved_objects.ts | 2 + src/plugins/data/server/server.api.md | 2 +- .../collectors/core/core_usage_collector.ts | 7 + src/plugins/telemetry/schema/oss_plugins.json | 21 + .../apis/saved_objects/bulk_create.ts | 11 + .../apis/saved_objects/bulk_get.ts | 9 + .../apis/saved_objects/create.ts | 39 + .../apis/saved_objects/export.ts | 10 + .../apis/saved_objects/find.ts | 13 +- .../api_integration/apis/saved_objects/get.ts | 8 + .../apis/saved_objects/index.ts | 5 +- .../lib/saved_objects_test_utils.ts | 18 + .../apis/saved_objects/migrations.ts | 346 +++- .../apis/saved_objects/resolve.ts | 104 + .../apis/saved_objects_management/find.ts | 10 + ...ypted_saved_objects_client_wrapper.test.ts | 134 ++ .../encrypted_saved_objects_client_wrapper.ts | 13 + .../server/audit/audit_events.test.ts | 18 + .../security/server/audit/audit_events.ts | 3 + ...ecure_saved_objects_client_wrapper.test.ts | 77 + .../secure_saved_objects_client_wrapper.ts | 36 + .../spaces_saved_objects_client.test.ts | 31 + .../spaces_saved_objects_client.ts | 22 + .../saved_objects/spaces/data.json | 116 ++ .../saved_objects/spaces/mappings.json | 29 + .../saved_object_test_plugin/server/plugin.ts | 7 + .../common/suites/resolve.ts | 138 ++ .../security_and_spaces/apis/index.ts | 1 + .../security_and_spaces/apis/resolve.ts | 82 + .../security_only/apis/index.ts | 1 + .../security_only/apis/resolve.ts | 73 + .../spaces_only/apis/index.ts | 1 + .../spaces_only/apis/resolve.ts | 47 + 118 files changed, 4859 insertions(+), 825 deletions(-) create mode 100644 docs/api/saved-objects/resolve.asciidoc create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobject.coremigrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.simplesavedobject.coremigrationversion.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobject.coremigrationversion.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md create mode 100644 src/core/server/saved_objects/migrations/core/__mocks__/index.ts create mode 100644 src/core/server/saved_objects/object_types/constants.ts create mode 100644 src/core/server/saved_objects/object_types/index.ts create mode 100644 src/core/server/saved_objects/object_types/registration.test.ts create mode 100644 src/core/server/saved_objects/object_types/registration.ts create mode 100644 src/core/server/saved_objects/object_types/types.ts create mode 100644 src/core/server/saved_objects/routes/integration_tests/resolve.test.ts create mode 100644 src/core/server/saved_objects/routes/resolve.ts create mode 100644 test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts create mode 100644 test/api_integration/apis/saved_objects/resolve.ts create mode 100644 x-pack/test/saved_object_api_integration/common/suites/resolve.ts create mode 100644 x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve.ts create mode 100644 x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts create mode 100644 x-pack/test/saved_object_api_integration/spaces_only/apis/resolve.ts diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index 0d8ceefb47e91..ecf975134c64a 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -10,6 +10,8 @@ The following saved objects APIs are available: * <> to retrieve a single {kib} saved object by ID +* <> to retrieve a single {kib} saved object by ID, using any legacy URL alias if it exists + * <> to retrieve multiple {kib} saved objects by ID * <> to retrieve a paginated set of {kib} saved objects by various conditions @@ -40,4 +42,5 @@ include::saved-objects/delete.asciidoc[] include::saved-objects/export.asciidoc[] include::saved-objects/import.asciidoc[] include::saved-objects/resolve_import_errors.asciidoc[] +include::saved-objects/resolve.asciidoc[] include::saved-objects/rotate_encryption_key.asciidoc[] diff --git a/docs/api/saved-objects/get.asciidoc b/docs/api/saved-objects/get.asciidoc index 6aad9759ef5e0..4c8cd020e0286 100644 --- a/docs/api/saved-objects/get.asciidoc +++ b/docs/api/saved-objects/get.asciidoc @@ -78,7 +78,7 @@ The API returns the following: "title": "[Flights] Global Flight Dashboard", "hits": 0, "description": "Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats", - "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":32,\"h\":7,\"i\":\"1\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":17,\"y\":7,\"w\":23,\"h\":12,\"i\":\"3\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Average Ticket Price\":\"#0A50A1\",\"Flight Count\":\"#82B5D8\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":0,\"y\":85,\"w\":48,\"h\":15,\"i\":\"4\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_2\"},{\"panelIndex\":\"5\",\"gridData\":{\"x\":0,\"y\":7,\"w\":17,\"h\":12,\"i\":\"5\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"ES-Air\":\"#447EBC\",\"JetBeats\":\"#65C5DB\",\"Kibana Airlines\":\"#BA43A9\",\"Logstash Airways\":\"#E5AC0E\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_3\"},{\"panelIndex\":\"6\",\"gridData\":{\"x\":24,\"y\":33,\"w\":24,\"h\":14,\"i\":\"6\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Carrier Delay\":\"#5195CE\",\"Late Aircraft Delay\":\"#1F78C1\",\"NAS Delay\":\"#70DBED\",\"No Delay\":\"#BADFF4\",\"Security Delay\":\"#052B51\",\"Weather Delay\":\"#6ED0E0\"}}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_4\"},{\"panelIndex\":\"7\",\"gridData\":{\"x\":24,\"y\":19,\"w\":24,\"h\":14,\"i\":\"7\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_5\"},{\"panelIndex\":\"10\",\"gridData\":{\"x\":0,\"y\":35,\"w\":24,\"h\":12,\"i\":\"10\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_6\"},{\"panelIndex\":\"13\",\"gridData\":{\"x\":10,\"y\":19,\"w\":14,\"h\":8,\"i\":\"13\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_7\"},{\"panelIndex\":\"14\",\"gridData\":{\"x\":10,\"y\":27,\"w\":14,\"h\":8,\"i\":\"14\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_8\"},{\"panelIndex\":\"18\",\"gridData\":{\"x\":24,\"y\":70,\"w\":24,\"h\":15,\"i\":\"18\"},\"embeddableConfig\":{\"mapCenter\":[27.421687059550266,15.371002131141724],\"mapZoom\":1},\"version\":\"6.3.0\",\"panelRefName\":\"panel_9\"},{\"panelIndex\":\"21\",\"gridData\":{\"x\":0,\"y\":62,\"w\":48,\"h\":8,\"i\":\"21\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_10\"},{\"panelIndex\":\"22\",\"gridData\":{\"x\":32,\"y\":0,\"w\":16,\"h\":7,\"i\":\"22\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_11\"},{\"panelIndex\":\"23\",\"gridData\":{\"x\":0,\"y\":70,\"w\":24,\"h\":15,\"i\":\"23\"},\"embeddableConfig\":{\"mapCenter\":[42.19556096274418,9.536742995308601e-7],\"mapZoom\":1},\"version\":\"6.3.0\",\"panelRefName\":\"panel_12\"},{\"panelIndex\":\"25\",\"gridData\":{\"x\":0,\"y\":19,\"w\":10,\"h\":8,\"i\":\"25\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(247,251,255)\",\"100 - 150\":\"rgb(107,174,214)\",\"150 - 200\":\"rgb(33,113,181)\",\"200 - 250\":\"rgb(8,48,107)\",\"50 - 100\":\"rgb(198,219,239)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_13\"},{\"panelIndex\":\"27\",\"gridData\":{\"x\":0,\"y\":27,\"w\":10,\"h\":8,\"i\":\"27\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(247,251,255)\",\"100 - 150\":\"rgb(107,174,214)\",\"150 - 200\":\"rgb(33,113,181)\",\"200 - 250\":\"rgb(8,48,107)\",\"50 - 100\":\"rgb(198,219,239)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_14\"},{\"panelIndex\":\"28\",\"gridData\":{\"x\":0,\"y\":47,\"w\":24,\"h\":15,\"i\":\"28\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 -* Connection #0 to host 69c72adb58fa46c69a01afdf4a6cbfd3.us-west1.gcp.cloud.es.io left intact\n 11\":\"rgb(247,251,255)\",\"11 - 22\":\"rgb(208,225,242)\",\"22 - 33\":\"rgb(148,196,223)\",\"33 - 44\":\"rgb(74,152,201)\",\"44 - 55\":\"rgb(23,100,171)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_15\"},{\"panelIndex\":\"29\",\"gridData\":{\"x\":40,\"y\":7,\"w\":8,\"h\":6,\"i\":\"29\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_16\"},{\"panelIndex\":\"30\",\"gridData\":{\"x\":40,\"y\":13,\"w\":8,\"h\":6,\"i\":\"30\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_17\"},{\"panelIndex\":\"31\",\"gridData\":{\"x\":24,\"y\":47,\"w\":24,\"h\":15,\"i\":\"31\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_18\"}]", + "panelsJSON": "[ . . . ]", "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", "version": 1, "timeRestore": true, diff --git a/docs/api/saved-objects/resolve.asciidoc b/docs/api/saved-objects/resolve.asciidoc new file mode 100644 index 0000000000000..f2bf31bc5d9e4 --- /dev/null +++ b/docs/api/saved-objects/resolve.asciidoc @@ -0,0 +1,130 @@ +[[saved-objects-api-resolve]] +=== Resolve object API +++++ +Resolve object +++++ + +experimental[] Retrieve a single {kib} saved object by ID, using any legacy URL alias if it exists. + +Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new +features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that +object can be retrieved via the Resolve API using either its new ID or its old ID. + +[[saved-objects-api-resolve-request]] +==== Request + +`GET :/api/saved_objects/resolve//` + +`GET :/s//api/saved_objects/resolve//` + +[[saved-objects-api-resolve-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + + +`type`:: + (Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`. + +`id`:: + (Required, string) The ID of the object to retrieve. + +[[saved-objects-api-resolve-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[saved-objects-api-resolve-example]] +==== Example + +Retrieve the index pattern object with the `my-pattern` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/saved_objects/resolve/index-pattern/my-pattern +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "saved_object": { + "id": "my-pattern", + "type": "index-pattern", + "version": 1, + "attributes": { + "title": "my-pattern-*" + } + }, + "outcome": "exactMatch" +} +-------------------------------------------------- + +The `outcome` field may be any of the following: + +* `"exactMatch"` -- One document exactly matched the given ID. +* `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID. +* `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID. + +Retrieve a dashboard object in the `testspace` by ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET s/testspace/api/saved_objects/resolve/dashboard/7adfa750-4c81-11e8-b3d7-01146121b73d +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "saved_object": { + "id": "7adfa750-4c81-11e8-b3d7-01146121b73d", + "type": "dashboard", + "updated_at": "2019-07-23T00:11:07.059Z", + "version": "WzQ0LDFd", + "attributes": { + "title": "[Flights] Global Flight Dashboard", + "hits": 0, + "description": "Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats", + "panelsJSON": "[ . . . ]", + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "version": 1, + "timeRestore": true, + "timeTo": "now", + "timeFrom": "now-24h", + "refreshInterval": { + "display": "15 minutes", + "pause": false, + "section": 2, + "value": 900000 + }, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + } + }, + "references": [ + { + "name": "panel_0", + "type": "visualization", + "id": "aeb212e0-4c84-11e8-b3d7-01146121b73d" + }, + . . . + { + "name": "panel_18", + "type": "visualization", + "id": "ed78a660-53a0-11e8-acbd-0be0ad9d822b" + } + ], + "migrationVersion": { + "dashboard": "7.0.0" + } + }, + "outcome": "conflict" +} +-------------------------------------------------- diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.coremigrationversion.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.coremigrationversion.md new file mode 100644 index 0000000000000..9060a5d6777fe --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.coremigrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md) + +## SavedObject.coreMigrationVersion property + +A semver value that is used when upgrading objects between Kibana versions. + +Signature: + +```typescript +coreMigrationVersion?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index eb6059747426d..9404927f94957 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -15,6 +15,7 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | +| [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | | [error](./kibana-plugin-core-public.savedobject.error.md) | SavedObjectError | | | [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md new file mode 100644 index 0000000000000..3c1d068f458bc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) > [coreMigrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md) + +## SavedObjectsCreateOptions.coreMigrationVersion property + +A semver value that is used when upgrading objects between Kibana versions. + +Signature: + +```typescript +coreMigrationVersion?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md index b1b93407d4ff1..a039b9f5b4fe4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsCreateOptions | Property | Type | Description | | --- | --- | --- | +| [coreMigrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | | [id](./kibana-plugin-core-public.savedobjectscreateoptions.id.md) | string | (Not recommended) Specify an id instead of having the saved objects service generate one for you. | | [migrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [overwrite](./kibana-plugin-core-public.savedobjectscreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md index b1a4357cca7ad..8fb005421e870 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `SimpleSavedObject` class Signature: ```typescript -constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType); +constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, }: SavedObjectType); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(client: SavedObjectsClientContract, { id, type, version, attributes, | Parameter | Type | Description | | --- | --- | --- | | client | SavedObjectsClientContract | | -| { id, type, version, attributes, error, references, migrationVersion } | SavedObjectType<T> | | +| { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, } | SavedObjectType<T> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.coremigrationversion.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.coremigrationversion.md new file mode 100644 index 0000000000000..8e2217fab6eee --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.coremigrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SimpleSavedObject](./kibana-plugin-core-public.simplesavedobject.md) > [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md) + +## SimpleSavedObject.coreMigrationVersion property + +Signature: + +```typescript +coreMigrationVersion: SavedObjectType['coreMigrationVersion']; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md index e9987f6d5bebb..35264a3a4cf0c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md @@ -18,7 +18,7 @@ export declare class SimpleSavedObject | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion })](./kibana-plugin-core-public.simplesavedobject._constructor_.md) | | Constructs a new instance of the SimpleSavedObject class | +| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, })](./kibana-plugin-core-public.simplesavedobject._constructor_.md) | | Constructs a new instance of the SimpleSavedObject class | ## Properties @@ -26,6 +26,7 @@ export declare class SimpleSavedObject | --- | --- | --- | --- | | [\_version](./kibana-plugin-core-public.simplesavedobject._version.md) | | SavedObjectType<T>['version'] | | | [attributes](./kibana-plugin-core-public.simplesavedobject.attributes.md) | | T | | +| [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md) | | SavedObjectType<T>['coreMigrationVersion'] | | | [error](./kibana-plugin-core-public.simplesavedobject.error.md) | | SavedObjectType<T>['error'] | | | [id](./kibana-plugin-core-public.simplesavedobject.id.md) | | SavedObjectType<T>['id'] | | | [migrationVersion](./kibana-plugin-core-public.simplesavedobject.migrationversion.md) | | SavedObjectType<T>['migrationVersion'] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 7daf5d086d9e4..4c6116540c12d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -139,7 +139,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributes](./kibana-plugin-core-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) | | | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | -| [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | +| [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | | [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | | [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | | @@ -187,10 +187,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | | | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsRawDoc](./kibana-plugin-core-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | +| [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) | Options that can be specified when using the saved objects serializer to parse a raw document. | | [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) | | | [SavedObjectsRemoveReferencesToResponse](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md) | | | [SavedObjectsRepositoryFactory](./kibana-plugin-core-server.savedobjectsrepositoryfactory.md) | Factory provided when invoking a [client factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) See [SavedObjectsServiceSetup.setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | +| [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) | | | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. | | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. | | [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) | Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.coremigrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.coremigrationversion.md new file mode 100644 index 0000000000000..b4d1f3c769451 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.coremigrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md) + +## SavedObject.coreMigrationVersion property + +A semver value that is used when upgrading objects between Kibana versions. + +Signature: + +```typescript +coreMigrationVersion?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 5aefc55736cd1..07172487e6fde 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -15,6 +15,7 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | +| [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | | [error](./kibana-plugin-core-server.savedobject.error.md) | SavedObjectError | | | [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md index 2ab9fcaf428b9..c07a41e28d45b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md @@ -4,7 +4,7 @@ ## SavedObjectMigrationMap interface -A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions. +A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version. For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md new file mode 100644 index 0000000000000..fb1f485cdf202 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) + +## SavedObjectsBulkCreateObject.coreMigrationVersion property + +A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. + +Signature: + +```typescript +coreMigrationVersion?: string; +``` + +## Remarks + +Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` field set and you want to create it again. + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index 5ac5f6d9807bd..6fc01212a2e41 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -16,6 +16,7 @@ export interface SavedObjectsBulkCreateObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | +| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | | [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 7fb34631c736e..da1f4d029ea2b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -36,5 +36,6 @@ The constructor for this class is marked as internal. Third-party code should no | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | +| [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md new file mode 100644 index 0000000000000..b9a63f0b8c05a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [resolve](./kibana-plugin-core-server.savedobjectsclient.resolve.md) + +## SavedObjectsClient.resolve() method + +Resolves a single object, using any legacy URL alias if it exists + +Signature: + +```typescript +resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | The type of SavedObject to retrieve | +| id | string | The ID of the SavedObject to retrieve | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md new file mode 100644 index 0000000000000..e2a4064ec4f33 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) + +## SavedObjectsCreateOptions.coreMigrationVersion property + +A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. + +Signature: + +```typescript +coreMigrationVersion?: string; +``` + +## Remarks + +Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` field set and you want to create it again. + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index e6d306784f8ae..1805f389d4e7f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | +| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | | [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md new file mode 100644 index 0000000000000..708d1bc9c514d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) + +## SavedObjectsRawDocParseOptions interface + +Options that can be specified when using the saved objects serializer to parse a raw document. + +Signature: + +```typescript +export interface SavedObjectsRawDocParseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespaceTreatment](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md) | 'strict' | 'lax' | Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations.If not specified, the default treatment is strict. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md new file mode 100644 index 0000000000000..c315d78aaf417 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) > [namespaceTreatment](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md) + +## SavedObjectsRawDocParseOptions.namespaceTreatment property + +Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations. + +If not specified, the default treatment is `strict`. + +Signature: + +```typescript +namespaceTreatment?: 'strict' | 'lax'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index c7e5b0476bad4..4d13fea12572c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -28,5 +28,6 @@ export declare class SavedObjectsRepository | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | +| [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md new file mode 100644 index 0000000000000..7d0a1c7d204be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [resolve](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) + +## SavedObjectsRepository.resolve() method + +Resolves a single object, using any legacy URL alias if it exists + +Signature: + +```typescript +resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + +{promise} - { saved\_object, outcome } + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md new file mode 100644 index 0000000000000..cfb309da0a716 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) + +## SavedObjectsResolveResponse interface + + +Signature: + +```typescript +export interface SavedObjectsResolveResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | +| [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md new file mode 100644 index 0000000000000..eadd85b175375 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) + +## SavedObjectsResolveResponse.outcome property + +The outcome for a successful `resolve` call is one of the following values: + +\* `'exactMatch'` -- One document exactly matched the given ID. \* `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID. \* `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID. + +Signature: + +```typescript +outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md new file mode 100644 index 0000000000000..c184312675f75 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) + +## SavedObjectsResolveResponse.saved\_object property + +Signature: + +```typescript +saved_object: SavedObject; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md new file mode 100644 index 0000000000000..d33f42ee2cf5f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) > [generateRawLegacyUrlAliasId](./kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md) + +## SavedObjectsSerializer.generateRawLegacyUrlAliasId() method + +Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias. + +Signature: + +```typescript +generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| namespace | string | | +| type | string | | +| id | string | | + +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md index b9033b00624cc..1094cc25ab557 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md @@ -9,14 +9,15 @@ Determines whether or not the raw document can be converted to a saved object. Signature: ```typescript -isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean; +isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| rawDoc | SavedObjectsRawDoc | | +| doc | SavedObjectsRawDoc | | +| options | SavedObjectsRawDocParseOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.md index 129e6d8bf90f8..c7fa5fc85c613 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.md @@ -23,7 +23,8 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | | [generateRawId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsserializer.generaterawid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document. | -| [isRawSavedObject(rawDoc)](./kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. | -| [rawToSavedObject(doc)](./kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. | +| [generateRawLegacyUrlAliasId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias. | +| [isRawSavedObject(doc, options)](./kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. | +| [rawToSavedObject(doc, options)](./kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. | | [savedObjectToRaw(savedObj)](./kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md) | | Converts a document from the saved object client format to the format that is stored in elasticsearch. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md index dc9a2ef85839f..3fc386f263141 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md @@ -9,7 +9,7 @@ Converts a document from the format that is stored in elasticsearch to the saved Signature: ```typescript -rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc; +rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; ``` ## Parameters @@ -17,6 +17,7 @@ rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc; | Parameter | Type | Description | | --- | --- | --- | | doc | SavedObjectsRawDoc | | +| options | SavedObjectsRawDocParseOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md new file mode 100644 index 0000000000000..064bd0b35699d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) + +## SavedObjectsType.convertToMultiNamespaceTypeVersion property + +If defined, objects of this type will be converted to multi-namespace objects when migrating to this version. + +Requirements: + +1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) + +Example of a single-namespace type in 7.10: + +```ts +{ + name: 'foo', + hidden: false, + namespaceType: 'single', + mappings: {...} +} + +``` +Example after converting to a multi-namespace type in 7.11: + +```ts +{ + name: 'foo', + hidden: false, + namespaceType: 'multiple', + mappings: {...}, + convertToMultiNamespaceTypeVersion: '7.11.0' +} + +``` +Note: a migration function can be optionally specified for the same version. + +Signature: + +```typescript +convertToMultiNamespaceTypeVersion?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index e5c3fa2b3e92d..eacad53be39fe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -19,6 +19,28 @@ This is only internal for now, and will only be public when we expose the regist | Property | Type | Description | | --- | --- | --- | | [convertToAliasScript](./kibana-plugin-core-server.savedobjectstype.converttoaliasscript.md) | string | If defined, will be used to convert the type to an alias. | +| [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) | string | If defined, objects of this type will be converted to multi-namespace objects when migrating to this version.Requirements:1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)Example of a single-namespace type in 7.10: +```ts +{ + name: 'foo', + hidden: false, + namespaceType: 'single', + mappings: {...} +} + +``` +Example after converting to a multi-namespace type in 7.11: +```ts +{ + name: 'foo', + hidden: false, + namespaceType: 'multiple', + mappings: {...}, + convertToMultiNamespaceTypeVersion: '7.11.0' +} + +``` +Note: a migration function can be optionally specified for the same version. | | [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | | [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | | [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 88f85eb7a7d05..8f1ea7b95a5f9 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index acb0f94cf878c..12a87b1422c5c 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -194,6 +194,10 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed a saved object. | `failure` | User is not authorized to access a saved object. +.2+| `saved_object_resolve` +| `success` | User has accessed a saved object. +| `failure` | User is not authorized to access a saved object. + .2+| `saved_object_find` | `success` | User has accessed a saved object as part of a search operation. | `failure` | User is not authorized to search for saved objects. diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index da818470133cd..0a166d4511c5f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1027,6 +1027,7 @@ export type PublicUiSettingsParams = Omit; // @public (undocumented) export interface SavedObject { attributes: T; + coreMigrationVersion?: string; // (undocumented) error?: SavedObjectError; id: string; @@ -1144,6 +1145,7 @@ export type SavedObjectsClientContract = PublicMethodsOf; // @public (undocumented) export interface SavedObjectsCreateOptions { + coreMigrationVersion?: string; id?: string; migrationVersion?: SavedObjectsMigrationVersion; overwrite?: boolean; @@ -1377,10 +1379,12 @@ export class ScopedHistory implements History { - constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); + constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, }: SavedObject); // (undocumented) attributes: T; // (undocumented) + coreMigrationVersion: SavedObject['coreMigrationVersion']; + // (undocumented) delete(): Promise<{}>; // (undocumented) error: SavedObject['error']; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 6c24cf2d0971b..fdef63c392db6 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -38,6 +38,8 @@ export interface SavedObjectsCreateOptions { overwrite?: boolean; /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** A semver value that is used when upgrading objects between Kibana versions. */ + coreMigrationVersion?: string; references?: SavedObjectReference[]; } diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index a0ebc8214aaec..0eb0e0b53f78e 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -27,12 +27,22 @@ export class SimpleSavedObject { public id: SavedObjectType['id']; public type: SavedObjectType['type']; public migrationVersion: SavedObjectType['migrationVersion']; + public coreMigrationVersion: SavedObjectType['coreMigrationVersion']; public error: SavedObjectType['error']; public references: SavedObjectType['references']; constructor( private client: SavedObjectsClientContract, - { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType + { + id, + type, + version, + attributes, + error, + references, + migrationVersion, + coreMigrationVersion, + }: SavedObjectType ) { this.id = id; this.type = type; @@ -40,6 +50,7 @@ export class SimpleSavedObject { this.references = references || []; this._version = version; this.migrationVersion = migrationVersion; + this.coreMigrationVersion = coreMigrationVersion; if (error) { this.error = error; } @@ -66,6 +77,7 @@ export class SimpleSavedObject { } else { return this.client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion, + coreMigrationVersion: this.coreMigrationVersion, references: this.references, }); } diff --git a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts index 8495f2e0d082a..8a0aaa646438d 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts @@ -18,6 +18,7 @@ const createUsageStatsClientMock = () => incrementSavedObjectsDelete: jest.fn().mockResolvedValue(null), incrementSavedObjectsFind: jest.fn().mockResolvedValue(null), incrementSavedObjectsGet: jest.fn().mockResolvedValue(null), + incrementSavedObjectsResolve: jest.fn().mockResolvedValue(null), incrementSavedObjectsUpdate: jest.fn().mockResolvedValue(null), incrementSavedObjectsImport: jest.fn().mockResolvedValue(null), incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null), diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts index 0e43363dddb77..2067466c63510 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.test.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -20,6 +20,7 @@ import { DELETE_STATS_PREFIX, FIND_STATS_PREFIX, GET_STATS_PREFIX, + RESOLVE_STATS_PREFIX, UPDATE_STATS_PREFIX, IMPORT_STATS_PREFIX, RESOLVE_IMPORT_STATS_PREFIX, @@ -594,6 +595,81 @@ describe('CoreUsageStatsClient', () => { }); }); + describe('#incrementSavedObjectsResolve', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsResolve({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolve({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_STATS_PREFIX}.total`, + `${RESOLVE_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsResolve({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_STATS_PREFIX}.total`, + `${RESOLVE_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolve({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_STATS_PREFIX}.total`, + `${RESOLVE_STATS_PREFIX}.namespace.custom.total`, + `${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + describe('#incrementSavedObjectsUpdate', () => { it('does not throw an error if repository incrementCounter operation fails', async () => { const { usageStatsClient, repositoryMock } = setup(); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index 103e98d2ef37e..70bdb99f666fd 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -40,6 +40,7 @@ export const CREATE_STATS_PREFIX = 'apiCalls.savedObjectsCreate'; export const DELETE_STATS_PREFIX = 'apiCalls.savedObjectsDelete'; export const FIND_STATS_PREFIX = 'apiCalls.savedObjectsFind'; export const GET_STATS_PREFIX = 'apiCalls.savedObjectsGet'; +export const RESOLVE_STATS_PREFIX = 'apiCalls.savedObjectsResolve'; export const UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsUpdate'; export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport'; export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors'; @@ -53,6 +54,7 @@ const ALL_COUNTER_FIELDS = [ ...getFieldsForCounter(DELETE_STATS_PREFIX), ...getFieldsForCounter(FIND_STATS_PREFIX), ...getFieldsForCounter(GET_STATS_PREFIX), + ...getFieldsForCounter(RESOLVE_STATS_PREFIX), ...getFieldsForCounter(UPDATE_STATS_PREFIX), // Saved Objects Management APIs ...getFieldsForCounter(IMPORT_STATS_PREFIX), @@ -123,6 +125,10 @@ export class CoreUsageStatsClient { await this.updateUsageStats([], GET_STATS_PREFIX, options); } + public async incrementSavedObjectsResolve(options: BaseIncrementOptions) { + await this.updateUsageStats([], RESOLVE_STATS_PREFIX, options); + } + public async incrementSavedObjectsUpdate(options: BaseIncrementOptions) { await this.updateUsageStats([], UPDATE_STATS_PREFIX, options); } diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index bd79e118c4460..505dd8528e755 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -66,6 +66,13 @@ export interface CoreUsageStats { 'apiCalls.savedObjectsGet.namespace.custom.total'?: number; 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes'?: number; 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolve.total'?: number; + 'apiCalls.savedObjectsResolve.namespace.default.total'?: number; + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolve.namespace.custom.total'?: number; + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsUpdate.total'?: number; 'apiCalls.savedObjectsUpdate.namespace.default.total'?: number; 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes'?: number; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0eb246b4c978b..a27863a458f2b 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -277,10 +277,12 @@ export { SavedObjectMigrationContext, SavedObjectsMigrationLogger, SavedObjectsRawDoc, + SavedObjectsRawDocParseOptions, SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, + SavedObjectsResolveResponse, SavedObjectsSerializer, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 57dee5cd51f1d..86ee7de5fab54 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -43,6 +43,7 @@ export { export { SavedObjectsSerializer, SavedObjectsRawDoc, + SavedObjectsRawDocParseOptions, SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc, } from './serialization'; diff --git a/src/core/server/saved_objects/migrations/core/__mocks__/index.ts b/src/core/server/saved_objects/migrations/core/__mocks__/index.ts new file mode 100644 index 0000000000000..b22ad0c93b234 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/__mocks__/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +const mockUuidv5 = jest.fn().mockReturnValue('uuidv5'); +Object.defineProperty(mockUuidv5, 'DNS', { value: 'DNSUUID', writable: false }); +jest.mock('uuid/v5', () => mockUuidv5); + +export { mockUuidv5 }; diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index f8ef47cae8944..9ee998118bde6 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -6,6 +6,7 @@ Object { "migrationMappingPropertyHashes": Object { "aaa": "625b32086eb1d1203564cf85062dd22e", "bbb": "18c78c995965207ed3f6e7fc5c6e55fe", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", @@ -23,6 +24,9 @@ Object { "bbb": Object { "type": "long", }, + "coreMigrationVersion": Object { + "type": "keyword", + }, "migrationVersion": Object { "dynamic": "true", "type": "object", @@ -64,6 +68,7 @@ exports[`buildActiveMappings handles the \`dynamic\` property of types 1`] = ` Object { "_meta": Object { "migrationMappingPropertyHashes": Object { + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "firstType": "635418ab953d81d93f1190b70a8d3f57", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", @@ -78,6 +83,9 @@ Object { }, "dynamic": "strict", "properties": Object { + "coreMigrationVersion": Object { + "type": "keyword", + }, "firstType": Object { "dynamic": "strict", "properties": Object { diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index 594c6e4e3df6a..83e7b1549bc97 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -153,6 +153,9 @@ function defaultMapping(): IndexMapping { }, }, }, + coreMigrationVersion: { + type: 'keyword', + }, }, }; } diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 9b97867bf187f..741f715ba6ebe 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -6,6 +6,7 @@ * Public License, v 1. */ +import { mockUuidv5 } from './__mocks__'; import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; @@ -13,9 +14,11 @@ import { DocumentMigrator } from './document_migrator'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectsType } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; const mockLoggerFactory = loggingSystemMock.create(); const mockLogger = mockLoggerFactory.get('mock logger'); +const kibanaVersion = '25.2.3'; const createRegistry = (...types: Array>) => { const registry = new SavedObjectTypeRegistry(); @@ -32,644 +35,1216 @@ const createRegistry = (...types: Array>) => { return registry; }; +beforeEach(() => { + mockUuidv5.mockClear(); +}); + describe('DocumentMigrator', () => { function testOpts() { return { - kibanaVersion: '25.2.3', + kibanaVersion, typeRegistry: createRegistry(), + minimumConvertVersion: '0.0.0', // no minimum version unless we specify it for a test case log: mockLogger, }; } - const createDefinition = (migrations: any) => ({ - kibanaVersion: '3.2.3', - typeRegistry: createRegistry({ - name: 'foo', - migrations: migrations as any, - }), - log: mockLogger, - }); + describe('validation', () => { + const createDefinition = (migrations: any) => ({ + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + migrations: migrations as any, + }), + log: mockLogger, + }); - it('validates migration definition', () => { - expect(() => new DocumentMigrator(createDefinition(() => {}))).not.toThrow(); - expect(() => new DocumentMigrator(createDefinition({}))).not.toThrow(); - expect(() => new DocumentMigrator(createDefinition(123))).toThrow( - /Migration for type foo should be an object or a function/i - ); - }); + describe('#prepareMigrations', () => { + it('validates individual migration definitions', () => { + const invalidMigrator = new DocumentMigrator(createDefinition(() => 123)); + const voidMigrator = new DocumentMigrator(createDefinition(() => {})); + const emptyObjectMigrator = new DocumentMigrator(createDefinition(() => ({}))); - describe('#prepareMigrations', () => { - it('validates individual migration definitions', () => { - const invalidMigrator = new DocumentMigrator(createDefinition(() => 123)); - const voidMigrator = new DocumentMigrator(createDefinition(() => {})); - const emptyObjectMigrator = new DocumentMigrator(createDefinition(() => ({}))); + expect(invalidMigrator.prepareMigrations).toThrow( + /Migrations map for type foo should be an object/i + ); + expect(voidMigrator.prepareMigrations).not.toThrow(); + expect(emptyObjectMigrator.prepareMigrations).not.toThrow(); + }); - expect(invalidMigrator.prepareMigrations).toThrow( - /Migrations map for type foo should be an object/i - ); - expect(voidMigrator.prepareMigrations).not.toThrow(); - expect(emptyObjectMigrator.prepareMigrations).not.toThrow(); - }); + it('validates individual migrations are valid semvers', () => { + const withInvalidVersion = { + bar: (doc: any) => doc, + '1.2.3': (doc: any) => doc, + }; + const migrationFn = new DocumentMigrator(createDefinition(() => withInvalidVersion)); + const migrationObj = new DocumentMigrator(createDefinition(withInvalidVersion)); - it('validates individual migration semvers', () => { - const withInvalidVersion = { - bar: (doc: any) => doc, - '1.2.3': (doc: any) => doc, - }; - const migrationFn = new DocumentMigrator(createDefinition(() => withInvalidVersion)); - const migrationObj = new DocumentMigrator(createDefinition(withInvalidVersion)); + expect(migrationFn.prepareMigrations).toThrow(/Expected all properties to be semvers/i); + expect(migrationObj.prepareMigrations).toThrow(/Expected all properties to be semvers/i); + }); - expect(migrationFn.prepareMigrations).toThrow(/Expected all properties to be semvers/i); - expect(migrationObj.prepareMigrations).toThrow(/Expected all properties to be semvers/i); - }); + it('validates individual migrations are not greater than the current Kibana version', () => { + const withGreaterVersion = { + '3.2.4': (doc: any) => doc, + }; + const migrationFn = new DocumentMigrator(createDefinition(() => withGreaterVersion)); + const migrationObj = new DocumentMigrator(createDefinition(withGreaterVersion)); - it('validates the migration function', () => { - const invalidVersionFunction = { '1.2.3': 23 as any }; - const migrationFn = new DocumentMigrator(createDefinition(() => invalidVersionFunction)); - const migrationObj = new DocumentMigrator(createDefinition(invalidVersionFunction)); + const expectedError = `Invalid migration for type foo. Property '3.2.4' cannot be greater than the current Kibana version '3.2.3'.`; + expect(migrationFn.prepareMigrations).toThrowError(expectedError); + expect(migrationObj.prepareMigrations).toThrowError(expectedError); + }); - expect(migrationFn.prepareMigrations).toThrow(/expected a function, but got 23/i); - expect(migrationObj.prepareMigrations).toThrow(/expected a function, but got 23/i); - }); - it('validates definitions with migrations: Function | Objects', () => { - const validMigrationMap = { '1.2.3': () => {} }; - const migrationFn = new DocumentMigrator(createDefinition(() => validMigrationMap)); - const migrationObj = new DocumentMigrator(createDefinition(validMigrationMap)); - expect(migrationFn.prepareMigrations).not.toThrow(); - expect(migrationObj.prepareMigrations).not.toThrow(); - }); - }); + it('validates the migration function', () => { + const invalidVersionFunction = { '1.2.3': 23 as any }; + const migrationFn = new DocumentMigrator(createDefinition(() => invalidVersionFunction)); + const migrationObj = new DocumentMigrator(createDefinition(invalidVersionFunction)); - it('throws if #prepareMigrations is not called before #migrate is called', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'user', - migrations: { - '1.2.3': setAttr('attributes.name', 'Chris'), - }, - }), + expect(migrationFn.prepareMigrations).toThrow(/expected a function, but got 23/i); + expect(migrationObj.prepareMigrations).toThrow(/expected a function, but got 23/i); + }); + it('validates definitions with migrations: Function | Objects', () => { + const validMigrationMap = { '1.2.3': () => {} }; + const migrationFn = new DocumentMigrator(createDefinition(() => validMigrationMap)); + const migrationObj = new DocumentMigrator(createDefinition(validMigrationMap)); + expect(migrationFn.prepareMigrations).not.toThrow(); + expect(migrationObj.prepareMigrations).not.toThrow(); + }); }); - expect(() => - migrator.migrate({ - id: 'me', - type: 'user', - attributes: { name: 'Christopher' }, - migrationVersion: {}, - }) - ).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i); - }); + it('throws if #prepareMigrations is not called before #migrate or #migrateAndConvert is called', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'user', + migrations: { + '1.2.3': setAttr('attributes.name', 'Chris'), + }, + }), + }); - it('migrates type and attributes', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'user', - migrations: { - '1.2.3': setAttr('attributes.name', 'Chris'), - }, - }), + expect(() => + migrator.migrate({ + id: 'me', + type: 'user', + attributes: { name: 'Christopher' }, + migrationVersion: {}, + }) + ).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i); + + expect(() => + migrator.migrateAndConvert({ + id: 'me', + type: 'user', + attributes: { name: 'Christopher' }, + migrationVersion: {}, + }) + ).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i); }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'me', - type: 'user', - attributes: { name: 'Christopher' }, - migrationVersion: {}, + it(`validates convertToMultiNamespaceTypeVersion can only be used with namespaceType 'multiple'`, () => { + const invalidDefinition = { + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: 'bar', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(invalidDefinition)).toThrow( + `Invalid convertToMultiNamespaceTypeVersion for type foo. Expected namespaceType to be 'multiple', but got 'single'.` + ); }); - expect(actual).toEqual({ - id: 'me', - type: 'user', - attributes: { name: 'Chris' }, - migrationVersion: { user: '1.2.3' }, + + it(`validates convertToMultiNamespaceTypeVersion must be a semver`, () => { + const invalidDefinition = { + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: 'bar', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(invalidDefinition)).toThrow( + `Invalid convertToMultiNamespaceTypeVersion for type foo. Expected value to be a semver, but got 'bar'.` + ); }); - }); - it(`doesn't mutate the original document`, () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'user', - migrations: { - '1.2.3': (doc) => { - set(doc, 'attributes.name', 'Mike'); - return doc; - }, - }, - }), + it('validates convertToMultiNamespaceTypeVersion is not less than the minimum allowed version', () => { + const invalidDefinition = { + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.2.4', + namespaceType: 'multiple', + }), + // not using a minimumConvertVersion parameter, the default is 8.0.0 + log: mockLogger, + }; + expect(() => new DocumentMigrator(invalidDefinition)).toThrowError( + `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be less than '8.0.0'.` + ); }); - const originalDoc = { - id: 'me', - type: 'user', - attributes: {}, - migrationVersion: {}, - }; - migrator.prepareMigrations(); - const migratedDoc = migrator.migrate(originalDoc); - expect(_.get(originalDoc, 'attributes.name')).toBeUndefined(); - expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike'); - }); - it('migrates root properties', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'acl', - migrations: { - '2.3.5': setAttr('acl', 'admins-only, sucka!'), - }, - }), + it('validates convertToMultiNamespaceTypeVersion is not greater than the current Kibana version', () => { + const invalidDefinition = { + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.2.4', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(invalidDefinition)).toThrowError( + `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be greater than the current Kibana version '3.2.3'.` + ); }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, - acl: 'anyone', - migrationVersion: {}, - } as SavedObjectUnsanitizedDoc); - expect(actual).toEqual({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, - migrationVersion: { acl: '2.3.5' }, - acl: 'admins-only, sucka!', + + it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => { + const invalidDefinition = { + kibanaVersion: '3.2.3', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.1.1', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(invalidDefinition)).toThrowError( + `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.1.1' cannot be used on a patch version (must be like 'x.y.0').` + ); }); }); - it('does not apply migrations to unrelated docs', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { - name: 'aaa', - migrations: { - '1.0.0': setAttr('aaa', 'A'), - }, - }, - { - name: 'bbb', - migrations: { - '1.0.0': setAttr('bbb', 'B'), - }, - }, - { - name: 'ccc', + describe('migration', () => { + it('migrates type and attributes', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'user', migrations: { - '1.0.0': setAttr('ccc', 'C'), + '1.2.3': setAttr('attributes.name', 'Chris'), }, - } - ), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, - migrationVersion: {}, - }); - expect(actual).toEqual({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'me', + type: 'user', + attributes: { name: 'Christopher' }, + migrationVersion: {}, + }); + expect(actual).toEqual({ + id: 'me', + type: 'user', + attributes: { name: 'Chris' }, + migrationVersion: { user: '1.2.3' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('assumes documents w/ undefined migrationVersion are up to date', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { + it(`doesn't mutate the original document`, () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ name: 'user', migrations: { - '1.0.0': setAttr('aaa', 'A'), - }, - }, - { - name: 'bbb', - migrations: { - '2.3.4': setAttr('bbb', 'B'), + '1.2.3': (doc) => { + set(doc, 'attributes.name', 'Mike'); + return doc; + }, }, - }, - { - name: 'ccc', + }), + }); + migrator.prepareMigrations(); + const originalDoc = { + id: 'me', + type: 'user', + attributes: {}, + migrationVersion: {}, + }; + const migratedDoc = migrator.migrate(originalDoc); + expect(_.get(originalDoc, 'attributes.name')).toBeUndefined(); + expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike'); + }); + + it('migrates root properties', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'acl', migrations: { - '1.0.0': setAttr('ccc', 'C'), + '2.3.5': setAttr('acl', 'admins-only, sucka!'), }, - } - ), + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + acl: 'anyone', + migrationVersion: {}, + } as SavedObjectUnsanitizedDoc); + expect(actual).toEqual({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + migrationVersion: { acl: '2.3.5' }, + acl: 'admins-only, sucka!', + coreMigrationVersion: kibanaVersion, + }); }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, - bbb: 'Shazm', - } as SavedObjectUnsanitizedDoc); - expect(actual).toEqual({ - id: 'me', - type: 'user', - attributes: { name: 'Tyler' }, - bbb: 'Shazm', - migrationVersion: { - user: '1.0.0', - bbb: '2.3.4', - }, + + it('does not apply migrations to unrelated docs', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'aaa', + migrations: { + '1.0.0': setAttr('aaa', 'A'), + }, + }, + { + name: 'bbb', + migrations: { + '1.0.0': setAttr('bbb', 'B'), + }, + }, + { + name: 'ccc', + migrations: { + '1.0.0': setAttr('ccc', 'C'), + }, + } + ), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + migrationVersion: {}, + }); + expect(actual).toEqual({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('only applies migrations that are more recent than the doc', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'dog', - migrations: { - '1.2.3': setAttr('attributes.a', 'A'), - '1.2.4': setAttr('attributes.b', 'B'), - '2.0.1': setAttr('attributes.c', 'C'), + it('assumes documents w/ undefined migrationVersion and correct coreMigrationVersion are up to date', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'user', + migrations: { + '1.0.0': setAttr('aaa', 'A'), + }, + }, + { + name: 'bbb', + migrations: { + '2.3.4': setAttr('bbb', 'B'), + }, + }, + { + name: 'ccc', + migrations: { + '1.0.0': setAttr('ccc', 'C'), + }, + } + ), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + bbb: 'Shazm', + coreMigrationVersion: kibanaVersion, + } as SavedObjectUnsanitizedDoc); + expect(actual).toEqual({ + id: 'me', + type: 'user', + attributes: { name: 'Tyler' }, + bbb: 'Shazm', + migrationVersion: { + user: '1.0.0', + bbb: '2.3.4', }, - }), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie' }, - migrationVersion: { dog: '1.2.3' }, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie', b: 'B', c: 'C' }, - migrationVersion: { dog: '2.0.1' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('rejects docs that belong to a newer Kibana instance', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - kibanaVersion: '8.0.1', - }); - migrator.prepareMigrations(); - expect(() => - migrator.migrate({ + it('only applies migrations that are more recent than the doc', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', + migrations: { + '1.2.3': setAttr('attributes.a', 'A'), + '1.2.4': setAttr('attributes.b', 'B'), + '2.0.1': setAttr('attributes.c', 'C'), + }, + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ id: 'smelly', type: 'dog', attributes: { name: 'Callie' }, - migrationVersion: { dog: '10.2.0' }, - }) - ).toThrow( - /Document "smelly" has property "dog" which belongs to a more recent version of Kibana \[10\.2\.0\]\. The last known version is \[undefined\]/i - ); - }); + migrationVersion: { dog: '1.2.3' }, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie', b: 'B', c: 'C' }, + migrationVersion: { dog: '2.0.1' }, + coreMigrationVersion: kibanaVersion, + }); + }); - it('rejects docs that belong to a newer plugin', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'dawg', - migrations: { - '1.2.3': setAttr('attributes.a', 'A'), - }, - }), + it('rejects docs with a migrationVersion[type] for a type that does not have any migrations defined', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + migrationVersion: { dog: '10.2.0' }, + }) + ).toThrow( + /Document "smelly" has property "dog" which belongs to a more recent version of Kibana \[10\.2\.0\]\. The last known version is \[undefined\]/i + ); }); - migrator.prepareMigrations(); - expect(() => - migrator.migrate({ - id: 'fleabag', - type: 'dawg', - attributes: { name: 'Callie' }, - migrationVersion: { dawg: '1.2.4' }, - }) - ).toThrow( - /Document "fleabag" has property "dawg" which belongs to a more recent version of Kibana \[1\.2\.4\]\. The last known version is \[1\.2\.3\]/i - ); - }); - it('applies migrations in order', () => { - let count = 0; - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'dog', - migrations: { - '2.2.4': setAttr('attributes.b', () => ++count), - '10.0.1': setAttr('attributes.c', () => ++count), - '1.2.3': setAttr('attributes.a', () => ++count), - }, - }), + it('rejects docs with a migrationVersion[type] for a type that does not have a migration >= that version defined', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dawg', + migrations: { + '1.2.3': setAttr('attributes.a', 'A'), + }, + }), + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'fleabag', + type: 'dawg', + attributes: { name: 'Callie' }, + migrationVersion: { dawg: '1.2.4' }, + }) + ).toThrow( + /Document "fleabag" has property "dawg" which belongs to a more recent version of Kibana \[1\.2\.4\]\. The last known version is \[1\.2\.3\]/i + ); }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie' }, - migrationVersion: { dog: '1.2.0' }, + + it('rejects docs that have an invalid coreMigrationVersion', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + kibanaVersion: '8.0.1', + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'happy', + type: 'dog', + attributes: { name: 'Callie' }, + coreMigrationVersion: 'not-a-semver', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Document \\"happy\\" has an invalid \\"coreMigrationVersion\\" [not-a-semver]. This must be a semver value."` + ); }); - expect(actual).toEqual({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie', a: 1, b: 2, c: 3 }, - migrationVersion: { dog: '10.0.1' }, + + it('rejects docs that have a coreMigrationVersion higher than the current Kibana version', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + kibanaVersion: '8.0.1', + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'wet', + type: 'dog', + attributes: { name: 'Callie' }, + coreMigrationVersion: '8.0.2', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Document \\"wet\\" has a \\"coreMigrationVersion\\" which belongs to a more recent version of Kibana [8.0.2]. The current version is [8.0.1]."` + ); }); - }); - it('allows props to be added', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { - name: 'animal', - migrations: { - '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), - }, - }, - { + it('applies migrations in order', () => { + let count = 0; + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ name: 'dog', migrations: { - '2.2.4': setAttr('animal', 'Doggie'), + '2.2.4': setAttr('attributes.b', () => ++count), + '10.0.1': setAttr('attributes.c', () => ++count), + '1.2.3': setAttr('attributes.a', () => ++count), }, - } - ), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie' }, - migrationVersion: { dog: '1.2.0' }, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie' }, - animal: 'Animal: Doggie', - migrationVersion: { animal: '1.0.0', dog: '2.2.4' }, + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + migrationVersion: { dog: '1.2.0' }, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie', a: 1, b: 2, c: 3 }, + migrationVersion: { dog: '10.0.1' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('allows props to be renamed', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { - name: 'animal', - migrations: { - '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), - '3.2.1': renameAttr('animal', 'dawg'), + it('allows props to be added', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'animal', + migrations: { + '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), + }, }, - }, - { - name: 'dawg', + { + name: 'dog', + migrations: { + '2.2.4': setAttr('animal', 'Doggie'), + }, + } + ), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + migrationVersion: { dog: '1.2.0' }, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + animal: 'Animal: Doggie', + migrationVersion: { animal: '1.0.0', dog: '2.2.4' }, + coreMigrationVersion: kibanaVersion, + }); + }); + + it('allows props to be renamed', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', migrations: { - '2.2.4': renameAttr('dawg', 'animal'), - '3.2.0': setAttr('dawg', (name: string) => `Dawg3.x: ${name}`), + '1.0.0': setAttr('attributes.name', (name: string) => `Name: ${name}`), + '1.0.1': renameAttr('attributes.name', 'attributes.title'), + '1.0.2': setAttr('attributes.title', (name: string) => `Title: ${name}`), }, - } - ), + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + migrationVersion: {}, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'dog', + attributes: { title: 'Title: Name: Callie' }, + migrationVersion: { dog: '1.0.2' }, + coreMigrationVersion: kibanaVersion, + }); }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'foo', - attributes: { name: 'Callie' }, - dawg: 'Yo', - migrationVersion: {}, - } as SavedObjectUnsanitizedDoc); - expect(actual).toEqual({ - id: 'smelly', - type: 'foo', - attributes: { name: 'Callie' }, - dawg: 'Dawg3.x: Animal: Yo', - migrationVersion: { animal: '3.2.1', dawg: '3.2.0' }, + + it('allows changing type', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'cat', + migrations: { + '1.0.0': setAttr('attributes.name', (name: string) => `Kitty ${name}`), + }, + }, + { + name: 'dog', + migrations: { + '2.2.4': setAttr('type', 'cat'), + }, + } + ), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'smelly', + type: 'dog', + attributes: { name: 'Callie' }, + migrationVersion: {}, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'cat', + attributes: { name: 'Kitty Callie' }, + migrationVersion: { dog: '2.2.4', cat: '1.0.0' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('allows changing type', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { + it('disallows updating a migrationVersion prop to a lower version', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ name: 'cat', migrations: { - '1.0.0': setAttr('attributes.name', (name: string) => `Kitty ${name}`), + '1.0.0': setAttr('migrationVersion.foo', '3.2.1'), }, - }, - { - name: 'dog', + }), + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'smelly', + type: 'cat', + attributes: { name: 'Boo' }, + migrationVersion: { foo: '4.5.6' }, + }) + ).toThrow( + /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to 3.2.1./ + ); + }); + + it('disallows removing a migrationVersion prop', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'cat', migrations: { - '2.2.4': setAttr('type', 'cat'), + '1.0.0': setAttr('migrationVersion', {}), }, - } - ), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'dog', - attributes: { name: 'Callie' }, - migrationVersion: {}, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Kitty Callie' }, - migrationVersion: { dog: '2.2.4', cat: '1.0.0' }, + }), + }); + migrator.prepareMigrations(); + expect(() => + migrator.migrate({ + id: 'smelly', + type: 'cat', + attributes: { name: 'Boo' }, + migrationVersion: { foo: '4.5.6' }, + }) + ).toThrow( + /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to undefined./ + ); }); - }); - it('disallows updating a migrationVersion prop to a lower version', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'cat', - migrations: { - '1.0.0': setAttr('migrationVersion.foo', '3.2.1'), - }, - }), + it('allows updating a migrationVersion prop to a later version', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'cat', + migrations: { + '1.0.0': setAttr('migrationVersion.cat', '2.9.1'), + '2.0.0': () => { + throw new Error('POW!'); + }, + '2.9.1': () => { + throw new Error('BANG!'); + }, + '3.0.0': setAttr('attributes.name', 'Shiny'), + }, + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ + id: 'smelly', + type: 'cat', + attributes: { name: 'Boo' }, + migrationVersion: { cat: '0.5.6' }, + }); + expect(actual).toEqual({ + id: 'smelly', + type: 'cat', + attributes: { name: 'Shiny' }, + migrationVersion: { cat: '3.0.0' }, + coreMigrationVersion: kibanaVersion, + }); }); - migrator.prepareMigrations(); - expect(() => - migrator.migrate({ + it('allows adding props to migrationVersion', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'cat', + migrations: { + '1.0.0': setAttr('migrationVersion.foo', '5.6.7'), + }, + }), + }); + migrator.prepareMigrations(); + const actual = migrator.migrate({ id: 'smelly', type: 'cat', attributes: { name: 'Boo' }, - migrationVersion: { foo: '4.5.6' }, - }) - ).toThrow( - /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to 3.2.1./ - ); - }); - - it('disallows removing a migrationVersion prop', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'cat', - migrations: { - '1.0.0': setAttr('migrationVersion', {}), - }, - }), - }); - migrator.prepareMigrations(); - expect(() => - migrator.migrate({ + migrationVersion: {}, + }); + expect(actual).toEqual({ id: 'smelly', type: 'cat', attributes: { name: 'Boo' }, - migrationVersion: { foo: '4.5.6' }, - }) - ).toThrow( - /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to undefined./ - ); - }); - - it('allows updating a migrationVersion prop to a later version', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'cat', - migrations: { - '1.0.0': setAttr('migrationVersion.cat', '2.9.1'), - '2.0.0': () => { - throw new Error('POW!'); - }, - '2.9.1': () => { - throw new Error('BANG!'); - }, - '3.0.0': setAttr('attributes.name', 'Shiny'), - }, - }), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Boo' }, - migrationVersion: { cat: '0.5.6' }, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Shiny' }, - migrationVersion: { cat: '3.0.0' }, + migrationVersion: { cat: '1.0.0', foo: '5.6.7' }, + coreMigrationVersion: kibanaVersion, + }); }); - }); - it('allows adding props to migrationVersion', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'cat', - migrations: { - '1.0.0': setAttr('migrationVersion.foo', '5.6.7'), - }, - }), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Boo' }, - migrationVersion: {}, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Boo' }, - migrationVersion: { cat: '1.0.0', foo: '5.6.7' }, - }); - }); - - it('logs the document and transform that failed', () => { - const log = mockLogger; - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'dog', - migrations: { - '1.2.3': () => { - throw new Error('Dang diggity!'); + it('logs the document and transform that failed', () => { + const log = mockLogger; + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', + migrations: { + '1.2.3': () => { + throw new Error('Dang diggity!'); + }, }, - }, - }), - log, - }); - const failedDoc = { - id: 'smelly', - type: 'dog', - attributes: {}, - migrationVersion: {}, - }; - try { + }), + log, + }); migrator.prepareMigrations(); - migrator.migrate(_.cloneDeep(failedDoc)); - expect('Did not throw').toEqual('But it should have!'); - } catch (error) { - expect(error.message).toMatch(/Dang diggity!/); - const warning = loggingSystemMock.collect(mockLoggerFactory).warn[0][0]; - expect(warning).toContain(JSON.stringify(failedDoc)); - expect(warning).toContain('dog:1.2.3'); - } - }); + const failedDoc = { + id: 'smelly', + type: 'dog', + attributes: {}, + migrationVersion: {}, + }; + try { + migrator.migrate(_.cloneDeep(failedDoc)); + expect('Did not throw').toEqual('But it should have!'); + } catch (error) { + expect(error.message).toMatch(/Dang diggity!/); + const warning = loggingSystemMock.collect(mockLoggerFactory).warn[0][0]; + expect(warning).toContain(JSON.stringify(failedDoc)); + expect(warning).toContain('dog:1.2.3'); + } + }); - it('logs message in transform function', () => { - const logTestMsg = '...said the joker to the thief'; - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'dog', - migrations: { - '1.2.3': (doc, { log }) => { - log.info(logTestMsg); - log.warning(logTestMsg); - return doc; + it('logs message in transform function', () => { + const logTestMsg = '...said the joker to the thief'; + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', + migrations: { + '1.2.3': (doc, { log }) => { + log.info(logTestMsg); + log.warning(logTestMsg); + return doc; + }, }, - }, - }), - log: mockLogger, + }), + log: mockLogger, + }); + migrator.prepareMigrations(); + const doc = { + id: 'joker', + type: 'dog', + attributes: {}, + migrationVersion: {}, + }; + migrator.migrate(doc); + expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg); + expect(loggingSystemMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg); }); - const doc = { - id: 'joker', - type: 'dog', - attributes: {}, - migrationVersion: {}, - }; - migrator.prepareMigrations(); - migrator.migrate(doc); - expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg); - expect(loggingSystemMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg); - }); - test('extracts the latest migration version info', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry( - { - name: 'aaa', - migrations: { - '1.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, - '10.4.0': (doc: SavedObjectUnsanitizedDoc) => doc, - '2.2.1': (doc: SavedObjectUnsanitizedDoc) => doc, + test('extracts the latest migration version info', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'aaa', + migrations: { + '1.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, + '10.4.0': (doc: SavedObjectUnsanitizedDoc) => doc, + '2.2.1': (doc: SavedObjectUnsanitizedDoc) => doc, + }, }, - }, - { - name: 'bbb', - migrations: { - '3.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, - '2.0.0': (doc: SavedObjectUnsanitizedDoc) => doc, + { + name: 'bbb', + migrations: { + '3.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, + '2.0.0': (doc: SavedObjectUnsanitizedDoc) => doc, + }, }, - } - ), + { + name: 'ccc', + namespaceType: 'multiple', + migrations: { + '9.0.0': (doc: SavedObjectUnsanitizedDoc) => doc, + }, + convertToMultiNamespaceTypeVersion: '11.0.0', // this results in reference transforms getting added to other types, but does not increase the migrationVersion of those types + } + ), + }); + migrator.prepareMigrations(); + expect(migrator.migrationVersion).toEqual({ + aaa: '10.4.0', + bbb: '3.2.3', + ccc: '11.0.0', + }); }); - migrator.prepareMigrations(); - expect(migrator.migrationVersion).toEqual({ - aaa: '10.4.0', - bbb: '3.2.3', + describe('conversion to multi-namespace type', () => { + it('assumes documents w/ undefined migrationVersion and correct coreMigrationVersion are up to date', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + // no migration transforms are defined, the migrationVersion will be derived from 'convertToMultiNamespaceTypeVersion' + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'mischievous', + type: 'dog', + attributes: { name: 'Ann' }, + coreMigrationVersion: kibanaVersion, + } as SavedObjectUnsanitizedDoc; + const actual = migrator.migrateAndConvert(obj); + expect(actual).toEqual([ + { + id: 'mischievous', + type: 'dog', + attributes: { name: 'Ann' }, + migrationVersion: { dog: '1.0.0' }, + coreMigrationVersion: kibanaVersion, + // there is no 'namespaces' field because no transforms were applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario + }, + ]); + }); + + it('skips reference transforms and conversion transforms when using `migrate`', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }, + { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'cowardly', + type: 'dog', + attributes: { name: 'Leslie' }, + migrationVersion: {}, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + namespace: 'foo-namespace', + }; + const actual = migrator.migrate(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual({ + id: 'cowardly', + type: 'dog', + attributes: { name: 'Leslie' }, + migrationVersion: { dog: '1.0.0' }, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + coreMigrationVersion: kibanaVersion, + namespace: 'foo-namespace', + // there is no 'namespaces' field because no conversion transform was applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario + }); + }); + + describe('correctly applies reference transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { name: 'dog', namespaceType: 'single' }, + { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'bad', + type: 'dog', + attributes: { name: 'Sweet Peach' }, + migrationVersion: {}, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'bad', + type: 'dog', + attributes: { name: 'Sweet Peach' }, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(1); + expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:toy:favorite', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'bad', + type: 'dog', + attributes: { name: 'Sweet Peach' }, + references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed + coreMigrationVersion: kibanaVersion, + namespace: 'foo-namespace', + }, + ]); + }); + }); + + describe('correctly applies conversion transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', + namespaceType: 'multiple', + convertToMultiNamespaceTypeVersion: '1.0.0', + }), + }); + migrator.prepareMigrations(); + const obj = { + id: 'loud', + type: 'dog', + attributes: { name: 'Wally' }, + migrationVersion: {}, + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'loud', + type: 'dog', + attributes: { name: 'Wally' }, + migrationVersion: { dog: '1.0.0' }, + coreMigrationVersion: kibanaVersion, + namespaces: ['default'], + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(1); + expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:loud', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'uuidv5', + type: 'dog', + attributes: { name: 'Wally' }, + migrationVersion: { dog: '1.0.0' }, + coreMigrationVersion: kibanaVersion, + namespaces: ['foo-namespace'], + originId: 'loud', + }, + { + id: 'foo-namespace:dog:loud', + type: LEGACY_URL_ALIAS_TYPE, + attributes: { + targetNamespace: 'foo-namespace', + targetType: 'dog', + targetId: 'uuidv5', + }, + migrationVersion: {}, + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + }); + + describe('correctly applies reference and conversion transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }, + { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'cute', + type: 'dog', + attributes: { name: 'Too' }, + migrationVersion: {}, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'cute', + type: 'dog', + attributes: { name: 'Too' }, + migrationVersion: { dog: '1.0.0' }, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change + coreMigrationVersion: kibanaVersion, + namespaces: ['default'], + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(2); + expect(mockUuidv5).toHaveBeenNthCalledWith(1, 'foo-namespace:toy:favorite', 'DNSUUID'); + expect(mockUuidv5).toHaveBeenNthCalledWith(2, 'foo-namespace:dog:cute', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'uuidv5', + type: 'dog', + attributes: { name: 'Too' }, + migrationVersion: { dog: '1.0.0' }, + references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed + coreMigrationVersion: kibanaVersion, + namespaces: ['foo-namespace'], + originId: 'cute', + }, + { + id: 'foo-namespace:dog:cute', + type: LEGACY_URL_ALIAS_TYPE, + attributes: { + targetNamespace: 'foo-namespace', + targetType: 'dog', + targetId: 'uuidv5', + }, + migrationVersion: {}, + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + }); + + describe('correctly applies reference and migration transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'dog', + namespaceType: 'single', + migrations: { + '1.0.0': setAttr('migrationVersion.dog', '2.0.0'), + '2.0.0': (doc) => doc, // noop + }, + }, + { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'sleepy', + type: 'dog', + attributes: { name: 'Patches' }, + migrationVersion: {}, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'sleepy', + type: 'dog', + attributes: { name: 'Patches' }, + migrationVersion: { dog: '2.0.0' }, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(1); + expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:toy:favorite', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'sleepy', + type: 'dog', + attributes: { name: 'Patches' }, + migrationVersion: { dog: '2.0.0' }, + references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed + coreMigrationVersion: kibanaVersion, + namespace: 'foo-namespace', + }, + ]); + }); + }); + + describe('correctly applies conversion and migration transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry({ + name: 'dog', + namespaceType: 'multiple', + migrations: { + '1.0.0': setAttr('migrationVersion.dog', '2.0.0'), + '2.0.0': (doc) => doc, // noop + }, + convertToMultiNamespaceTypeVersion: '1.0.0', // the conversion transform occurs before the migration transform above + }), + }); + migrator.prepareMigrations(); + const obj = { + id: 'hungry', + type: 'dog', + attributes: { name: 'Remy' }, + migrationVersion: {}, + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'hungry', + type: 'dog', + attributes: { name: 'Remy' }, + migrationVersion: { dog: '2.0.0' }, + coreMigrationVersion: kibanaVersion, + namespaces: ['default'], + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(1); + expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:hungry', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'uuidv5', + type: 'dog', + attributes: { name: 'Remy' }, + migrationVersion: { dog: '2.0.0' }, + coreMigrationVersion: kibanaVersion, + namespaces: ['foo-namespace'], + originId: 'hungry', + }, + { + id: 'foo-namespace:dog:hungry', + type: LEGACY_URL_ALIAS_TYPE, + attributes: { + targetNamespace: 'foo-namespace', + targetType: 'dog', + targetId: 'uuidv5', + }, + migrationVersion: {}, + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + }); + + describe('correctly applies reference, conversion, and migration transforms', () => { + const migrator = new DocumentMigrator({ + ...testOpts(), + typeRegistry: createRegistry( + { + name: 'dog', + namespaceType: 'multiple', + migrations: { + '1.0.0': setAttr('migrationVersion.dog', '2.0.0'), + '2.0.0': (doc) => doc, // noop + }, + convertToMultiNamespaceTypeVersion: '1.0.0', + }, + { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } + ), + }); + migrator.prepareMigrations(); + const obj = { + id: 'pretty', + type: 'dog', + attributes: { name: 'Sasha' }, + migrationVersion: {}, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + }; + + it('in the default space', () => { + const actual = migrator.migrateAndConvert(obj); + expect(mockUuidv5).not.toHaveBeenCalled(); + expect(actual).toEqual([ + { + id: 'pretty', + type: 'dog', + attributes: { name: 'Sasha' }, + migrationVersion: { dog: '2.0.0' }, + references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change + coreMigrationVersion: kibanaVersion, + namespaces: ['default'], + }, + ]); + }); + + it('in a non-default space', () => { + const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' }); + expect(mockUuidv5).toHaveBeenCalledTimes(2); + expect(mockUuidv5).toHaveBeenNthCalledWith(1, 'foo-namespace:toy:favorite', 'DNSUUID'); + expect(mockUuidv5).toHaveBeenNthCalledWith(2, 'foo-namespace:dog:pretty', 'DNSUUID'); + expect(actual).toEqual([ + { + id: 'uuidv5', + type: 'dog', + attributes: { name: 'Sasha' }, + migrationVersion: { dog: '2.0.0' }, + references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed + coreMigrationVersion: kibanaVersion, + namespaces: ['foo-namespace'], + originId: 'pretty', + }, + { + id: 'foo-namespace:dog:pretty', + type: LEGACY_URL_ALIAS_TYPE, + attributes: { + targetNamespace: 'foo-namespace', + targetType: 'dog', + targetId: 'uuidv5', + }, + migrationVersion: {}, + coreMigrationVersion: kibanaVersion, + }, + ]); + }); + }); }); }); }); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 04e9a4e165f96..e4b89a949d3cf 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -50,50 +50,102 @@ */ import Boom from '@hapi/boom'; +import uuidv5 from 'uuid/v5'; import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import Semver from 'semver'; import { Logger } from '../../../logging'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; -import { SavedObjectsMigrationVersion } from '../../types'; +import { + SavedObjectsMigrationVersion, + SavedObjectsNamespaceType, + SavedObjectsType, +} from '../../types'; import { MigrationLogger } from './migration_logger'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types'; +import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils'; +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; +const DEFAULT_MINIMUM_CONVERT_VERSION = '8.0.0'; + +export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; +export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[]; + +interface TransformResult { + /** + * This is the original document that has been transformed. + */ + transformedDoc: SavedObjectUnsanitizedDoc; + /** + * These are any new document(s) that have been created during the transformation process; these are not transformed, but they are marked + * as up-to-date. Only conversion transforms generate additional documents. + */ + additionalDocs: SavedObjectUnsanitizedDoc[]; +} + +type ApplyTransformsFn = ( + doc: SavedObjectUnsanitizedDoc, + options?: TransformOptions +) => TransformResult; + +interface TransformOptions { + convertNamespaceTypes?: boolean; +} interface DocumentMigratorOptions { kibanaVersion: string; typeRegistry: ISavedObjectTypeRegistry; + minimumConvertVersion?: string; log: Logger; } interface ActiveMigrations { [type: string]: { - latestVersion: string; - transforms: Array<{ - version: string; - transform: TransformFn; - }>; + /** Derived from `migrate` transforms and `convert` transforms */ + latestMigrationVersion?: string; + /** Derived from `reference` transforms */ + latestCoreMigrationVersion?: string; + transforms: Transform[]; }; } +interface Transform { + version: string; + transform: (doc: SavedObjectUnsanitizedDoc) => TransformResult; + /** + * There are two "migrationVersion" transform types: + * * `migrate` - These transforms are defined and added by consumers using the type registry; each is applied to a single object type + * based on an object's `migrationVersion[type]` field. These are applied during index migrations and document migrations. + * * `convert` - These transforms are defined by core and added by consumers using the type registry; each is applied to a single object + * type based on an object's `migrationVersion[type]` field. These are applied during index migrations, NOT document migrations. + * + * There is one "coreMigrationVersion" transform type: + * * `reference` - These transforms are defined by core and added by consumers using the type registry; they are applied to all object + * types based on their `coreMigrationVersion` field. These are applied during index migrations, NOT document migrations. + * + * If any additional transform types are added, the functions below should be updated to account for them. + */ + transformType: 'migrate' | 'convert' | 'reference'; +} + /** * Manages migration of individual documents. */ export interface VersionedTransformer { migrationVersion: SavedObjectsMigrationVersion; + migrate: MigrateFn; + migrateAndConvert: MigrateAndConvertFn; prepareMigrations: () => void; - migrate: TransformFn; } /** * A concrete implementation of the VersionedTransformer interface. */ export class DocumentMigrator implements VersionedTransformer { - private documentMigratorOptions: DocumentMigratorOptions; + private documentMigratorOptions: Omit; private migrations?: ActiveMigrations; - private transformDoc?: TransformFn; + private transformDoc?: ApplyTransformsFn; /** * Creates an instance of DocumentMigrator. @@ -101,11 +153,18 @@ export class DocumentMigrator implements VersionedTransformer { * @param {DocumentMigratorOptions} opts * @prop {string} kibanaVersion - The current version of Kibana * @prop {SavedObjectTypeRegistry} typeRegistry - The type registry to get type migrations from + * @prop {string} minimumConvertVersion - The minimum version of Kibana in which documents can be converted to multi-namespace types * @prop {Logger} log - The migration logger * @memberof DocumentMigrator */ - constructor({ typeRegistry, kibanaVersion, log }: DocumentMigratorOptions) { - validateMigrationDefinition(typeRegistry); + constructor({ + typeRegistry, + kibanaVersion, + minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION, + log, + }: DocumentMigratorOptions) { + validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion); + this.documentMigratorOptions = { typeRegistry, kibanaVersion, log }; } @@ -120,7 +179,14 @@ export class DocumentMigrator implements VersionedTransformer { if (!this.migrations) { throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.'); } - return _.mapValues(this.migrations, ({ latestVersion }) => latestVersion); + + return Object.entries(this.migrations).reduce((acc, [prop, { latestMigrationVersion }]) => { + // some migration objects won't have a latestMigrationVersion (they only contain reference transforms that are applied from other types) + if (latestMigrationVersion) { + return { ...acc, [prop]: latestMigrationVersion }; + } + return acc; + }, {}); } /** @@ -132,7 +198,7 @@ export class DocumentMigrator implements VersionedTransformer { public prepareMigrations = () => { const { typeRegistry, kibanaVersion, log } = this.documentMigratorOptions; - this.migrations = buildActiveMigrations(typeRegistry, log); + this.migrations = buildActiveMigrations(typeRegistry, kibanaVersion, log); this.transformDoc = buildDocumentTransform({ kibanaVersion, migrations: this.migrations, @@ -155,25 +221,56 @@ export class DocumentMigrator implements VersionedTransformer { // Ex: Importing sample data that is cached at import level, migrations would // execute on mutated data the second time. const clonedDoc = _.cloneDeep(doc); - return this.transformDoc(clonedDoc); + const { transformedDoc } = this.transformDoc(clonedDoc); + return transformedDoc; + }; + + /** + * Migrates a document to the latest version and applies type conversions if applicable. Also returns any additional document(s) that may + * have been created during the transformation process. + * + * @param {SavedObjectUnsanitizedDoc} doc + * @returns {SavedObjectUnsanitizedDoc} + * @memberof DocumentMigrator + */ + public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => { + if (!this.migrations || !this.transformDoc) { + throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.'); + } + + // Clone the document to prevent accidental mutations on the original data + // Ex: Importing sample data that is cached at import level, migrations would + // execute on mutated data the second time. + const clonedDoc = _.cloneDeep(doc); + const { transformedDoc, additionalDocs } = this.transformDoc(clonedDoc, { + convertNamespaceTypes: true, + }); + return [transformedDoc, ...additionalDocs]; }; } -function validateMigrationsMapObject(name: string, migrationsMap?: SavedObjectMigrationMap) { +function validateMigrationsMapObject( + name: string, + kibanaVersion: string, + migrationsMap?: SavedObjectMigrationMap +) { function assertObject(obj: any, prefix: string) { if (!obj || typeof obj !== 'object') { throw new Error(`${prefix} Got ${obj}.`); } } - function assertValidSemver(version: string, type: string) { if (!Semver.valid(version)) { throw new Error( `Invalid migration for type ${type}. Expected all properties to be semvers, but got ${version}.` ); } + if (Semver.gt(version, kibanaVersion)) { + throw new Error( + `Invalid migration for type ${type}. Property '${version}' cannot be greater than the current Kibana version '${kibanaVersion}'.` + ); + } } - function assertValidTransform(fn: any, version: string, type: string) { if (typeof fn !== 'function') { throw new Error(`Invalid migration ${type}.${version}: expected a function, but got ${fn}.`); @@ -194,23 +291,63 @@ function validateMigrationsMapObject(name: string, migrationsMap?: SavedObjectMi } /** - * Basic validation that the migraiton definition matches our expectations. We can't + * Basic validation that the migration definition matches our expectations. We can't * rely on TypeScript here, as the caller may be JavaScript / ClojureScript / any compile-to-js * language. So, this is just to provide a little developer-friendly error messaging. Joi was * giving weird errors, so we're just doing manual validation. */ -function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) { +function validateMigrationDefinition( + registry: ISavedObjectTypeRegistry, + kibanaVersion: string, + minimumConvertVersion: string +) { function assertObjectOrFunction(entity: any, prefix: string) { if (!entity || (typeof entity !== 'function' && typeof entity !== 'object')) { throw new Error(`${prefix} Got! ${typeof entity}, ${JSON.stringify(entity)}.`); } } + function assertValidConvertToMultiNamespaceType( + namespaceType: SavedObjectsNamespaceType, + convertToMultiNamespaceTypeVersion: string, + type: string + ) { + if (namespaceType !== 'multiple') { + throw new Error( + `Invalid convertToMultiNamespaceTypeVersion for type ${type}. Expected namespaceType to be 'multiple', but got '${namespaceType}'.` + ); + } else if (!Semver.valid(convertToMultiNamespaceTypeVersion)) { + throw new Error( + `Invalid convertToMultiNamespaceTypeVersion for type ${type}. Expected value to be a semver, but got '${convertToMultiNamespaceTypeVersion}'.` + ); + } else if (Semver.lt(convertToMultiNamespaceTypeVersion, minimumConvertVersion)) { + throw new Error( + `Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be less than '${minimumConvertVersion}'.` + ); + } else if (Semver.gt(convertToMultiNamespaceTypeVersion, kibanaVersion)) { + throw new Error( + `Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be greater than the current Kibana version '${kibanaVersion}'.` + ); + } else if (Semver.patch(convertToMultiNamespaceTypeVersion)) { + throw new Error( + `Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be used on a patch version (must be like 'x.y.0').` + ); + } + } + registry.getAllTypes().forEach((type) => { - if (type.migrations) { + const { name, migrations, convertToMultiNamespaceTypeVersion, namespaceType } = type; + if (migrations) { assertObjectOrFunction( type.migrations, - `Migration for type ${type.name} should be an object or a function returning an object like { '2.0.0': (doc) => doc }.` + `Migration for type ${name} should be an object or a function returning an object like { '2.0.0': (doc) => doc }.` + ); + } + if (convertToMultiNamespaceTypeVersion) { + assertValidConvertToMultiNamespaceType( + namespaceType, + convertToMultiNamespaceTypeVersion, + name ); } }); @@ -220,74 +357,144 @@ function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) { * Converts migrations from a format that is convenient for callers to a format that * is convenient for our internal usage: * From: { type: { version: fn } } - * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } + * To: { type: { latestMigrationVersion?: string; latestCoreMigrationVersion?: string; transforms: [{ version: string, transform: fn }] } } */ function buildActiveMigrations( typeRegistry: ISavedObjectTypeRegistry, + kibanaVersion: string, log: Logger ): ActiveMigrations { - const typesWithMigrationMaps = typeRegistry - .getAllTypes() - .map((type) => ({ - ...type, - migrationsMap: typeof type.migrations === 'function' ? type.migrations() : type.migrations, - })) - .filter((type) => typeof type.migrationsMap !== 'undefined'); - - typesWithMigrationMaps.forEach((type) => - validateMigrationsMapObject(type.name, type.migrationsMap) - ); + const referenceTransforms = getReferenceTransforms(typeRegistry); + + return typeRegistry.getAllTypes().reduce((migrations, type) => { + const migrationsMap = + typeof type.migrations === 'function' ? type.migrations() : type.migrations; + validateMigrationsMapObject(type.name, kibanaVersion, migrationsMap); + + const migrationTransforms = Object.entries(migrationsMap ?? {}).map( + ([version, transform]) => ({ + version, + transform: wrapWithTry(version, type.name, transform, log), + transformType: 'migrate', + }) + ); + const conversionTransforms = getConversionTransforms(type); + const transforms = [ + ...referenceTransforms, + ...conversionTransforms, + ...migrationTransforms, + ].sort(transformComparator); + + if (!transforms.length) { + return migrations; + } - return typesWithMigrationMaps - .filter((type) => type.migrationsMap && Object.keys(type.migrationsMap).length > 0) - .reduce((migrations, type) => { - const transforms = Object.entries(type.migrationsMap!) - .map(([version, transform]) => ({ - version, - transform: wrapWithTry(version, type.name, transform, log), - })) - .sort((a, b) => Semver.compare(a.version, b.version)); - return { - ...migrations, - [type.name]: { - latestVersion: _.last(transforms)!.version, - transforms, - }, - }; - }, {} as ActiveMigrations); + const migrationVersionTransforms: Transform[] = []; + const coreMigrationVersionTransforms: Transform[] = []; + transforms.forEach((x) => { + if (x.transformType === 'migrate' || x.transformType === 'convert') { + migrationVersionTransforms.push(x); + } else { + coreMigrationVersionTransforms.push(x); + } + }); + + return { + ...migrations, + [type.name]: { + latestMigrationVersion: _.last(migrationVersionTransforms)?.version, + latestCoreMigrationVersion: _.last(coreMigrationVersionTransforms)?.version, + transforms, + }, + }; + }, {} as ActiveMigrations); } + /** * Creates a function which migrates and validates any document that is passed to it. */ function buildDocumentTransform({ + kibanaVersion, migrations, }: { kibanaVersion: string; migrations: ActiveMigrations; -}): TransformFn { - return function transformAndValidate(doc: SavedObjectUnsanitizedDoc) { - const result = doc.migrationVersion - ? applyMigrations(doc, migrations) - : markAsUpToDate(doc, migrations); +}): ApplyTransformsFn { + return function transformAndValidate( + doc: SavedObjectUnsanitizedDoc, + options: TransformOptions = {} + ) { + validateCoreMigrationVersion(doc, kibanaVersion); + + const { convertNamespaceTypes = false } = options; + let transformedDoc: SavedObjectUnsanitizedDoc; + let additionalDocs: SavedObjectUnsanitizedDoc[] = []; + if (doc.migrationVersion) { + const result = applyMigrations(doc, migrations, kibanaVersion, convertNamespaceTypes); + transformedDoc = result.transformedDoc; + additionalDocs = additionalDocs.concat( + result.additionalDocs.map((x) => markAsUpToDate(x, migrations, kibanaVersion)) + ); + } else { + transformedDoc = markAsUpToDate(doc, migrations, kibanaVersion); + } // In order to keep tests a bit more stable, we won't // tack on an empy migrationVersion to docs that have // no migrations defined. - if (_.isEmpty(result.migrationVersion)) { - delete result.migrationVersion; + if (_.isEmpty(transformedDoc.migrationVersion)) { + delete transformedDoc.migrationVersion; } - return result; + return { transformedDoc, additionalDocs }; }; } -function applyMigrations(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { +function validateCoreMigrationVersion(doc: SavedObjectUnsanitizedDoc, kibanaVersion: string) { + const { id, coreMigrationVersion: docVersion } = doc; + if (!docVersion) { + return; + } + + // We verify that the object's coreMigrationVersion is valid, and that it is not greater than the version supported by Kibana. + // If we have a coreMigrationVersion and the kibanaVersion is smaller than it or does not exist, we are dealing with a document that + // belongs to a future Kibana / plugin version. + if (!Semver.valid(docVersion)) { + throw Boom.badData( + `Document "${id}" has an invalid "coreMigrationVersion" [${docVersion}]. This must be a semver value.`, + doc + ); + } + + if (doc.coreMigrationVersion && Semver.gt(docVersion, kibanaVersion)) { + throw Boom.badData( + `Document "${id}" has a "coreMigrationVersion" which belongs to a more recent version` + + ` of Kibana [${docVersion}]. The current version is [${kibanaVersion}].`, + doc + ); + } +} + +function applyMigrations( + doc: SavedObjectUnsanitizedDoc, + migrations: ActiveMigrations, + kibanaVersion: string, + convertNamespaceTypes: boolean +) { + let additionalDocs: SavedObjectUnsanitizedDoc[] = []; while (true) { const prop = nextUnmigratedProp(doc, migrations); if (!prop) { - return doc; + // regardless of whether or not any reference transform was applied, update the coreMigrationVersion + // this is needed to ensure that newly created documents have an up-to-date coreMigrationVersion field + return { + transformedDoc: { ...doc, coreMigrationVersion: kibanaVersion }, + additionalDocs, + }; } - doc = migrateProp(doc, prop, migrations); + const result = migrateProp(doc, prop, migrations, convertNamespaceTypes); + doc = result.transformedDoc; + additionalDocs = [...additionalDocs, ...result.additionalDocs]; } } @@ -303,7 +510,7 @@ function props(doc: SavedObjectUnsanitizedDoc) { */ function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: string) { return ( - ((doc as any)[prop] && (doc as any)[prop].latestVersion) || + ((doc as any)[prop] && (doc as any)[prop].latestMigrationVersion) || (doc.migrationVersion && (doc as any).migrationVersion[prop]) ); } @@ -311,16 +518,137 @@ function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: st /** * Sets the doc's migrationVersion to be the most recent version */ -function markAsUpToDate(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { +function markAsUpToDate( + doc: SavedObjectUnsanitizedDoc, + migrations: ActiveMigrations, + kibanaVersion: string +) { return { ...doc, migrationVersion: props(doc).reduce((acc, prop) => { const version = propVersion(migrations, prop); return version ? set(acc, prop, version) : acc; }, {}), + coreMigrationVersion: kibanaVersion, }; } +/** + * Converts a single-namespace object to a multi-namespace object. This primarily entails removing the `namespace` field and adding the + * `namespaces` field. + * + * If the object does not exist in the default namespace (undefined), its ID is also regenerated, and an "originId" is added to preserve + * legacy import/copy behavior. + */ +function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) { + const { namespace, ...otherAttrs } = doc; + const additionalDocs: SavedObjectUnsanitizedDoc[] = []; + + // If this object exists in the default namespace, return it with the appropriate `namespaces` field without changing its ID. + if (namespace === undefined) { + return { + transformedDoc: { ...otherAttrs, namespaces: [DEFAULT_NAMESPACE_STRING] }, + additionalDocs, + }; + } + + const { id: originId, type } = otherAttrs; + const id = deterministicallyRegenerateObjectId(namespace, type, originId!); + if (namespace !== undefined) { + const legacyUrlAlias: SavedObjectUnsanitizedDoc = { + id: `${namespace}:${type}:${originId}`, + type: LEGACY_URL_ALIAS_TYPE, + attributes: { + targetNamespace: namespace, + targetType: type, + targetId: id, + }, + }; + additionalDocs.push(legacyUrlAlias); + } + return { + transformedDoc: { ...otherAttrs, id, originId, namespaces: [namespace] }, + additionalDocs, + }; +} + +/** + * Returns all applicable conversion transforms for a given object type. + */ +function getConversionTransforms(type: SavedObjectsType): Transform[] { + const { convertToMultiNamespaceTypeVersion } = type; + if (!convertToMultiNamespaceTypeVersion) { + return []; + } + return [ + { + version: convertToMultiNamespaceTypeVersion, + transform: convertNamespaceType, + transformType: 'convert', + }, + ]; +} + +/** + * Returns all applicable reference transforms for all object types. + */ +function getReferenceTransforms(typeRegistry: ISavedObjectTypeRegistry): Transform[] { + const transformMap = typeRegistry + .getAllTypes() + .filter((type) => type.convertToMultiNamespaceTypeVersion) + .reduce((acc, { convertToMultiNamespaceTypeVersion: version, name }) => { + const types = acc.get(version!) ?? new Set(); + return acc.set(version!, types.add(name)); + }, new Map>()); + + return Array.from(transformMap, ([version, types]) => ({ + version, + transform: (doc) => { + const { namespace, references } = doc; + if (namespace && references?.length) { + return { + transformedDoc: { + ...doc, + references: references.map(({ type, id, ...attrs }) => ({ + ...attrs, + type, + id: types.has(type) ? deterministicallyRegenerateObjectId(namespace, type, id) : id, + })), + }, + additionalDocs: [], + }; + } + return { transformedDoc: doc, additionalDocs: [] }; + }, + transformType: 'reference', + })); +} + +/** + * Transforms are sorted in ascending order by version. One version may contain multiple transforms; 'reference' transforms always run + * first, 'convert' transforms always run second, and 'migrate' transforms always run last. This is because: + * 1. 'convert' transforms get rid of the `namespace` field, which must be present for 'reference' transforms to function correctly. + * 2. 'migrate' transforms are defined by the consumer, and may change the object type or migrationVersion which resets the migration loop + * and could cause any remaining transforms for this version to be skipped. + */ +function transformComparator(a: Transform, b: Transform) { + const semver = Semver.compare(a.version, b.version); + if (semver !== 0) { + return semver; + } else if (a.transformType !== b.transformType) { + if (a.transformType === 'migrate') { + return 1; + } else if (b.transformType === 'migrate') { + return -1; + } else if (a.transformType === 'convert') { + return 1; + } else if (b.transformType === 'convert') { + return -1; + } + } + return 0; +} + /** * If a specific transform function fails, this tacks on a bit of information * about the document and transform that caused the failure. @@ -342,7 +670,7 @@ function wrapWithTry( throw new Error(`Invalid saved object returned from migration ${type}:${version}.`); } - return result; + return { transformedDoc: result, additionalDocs: [] }; } catch (error) { const failedTransform = `${type}:${version}`; const failedDoc = JSON.stringify(doc); @@ -354,32 +682,52 @@ function wrapWithTry( }; } +/** + * Determines whether or not a document has any pending transforms that should be applied based on its coreMigrationVersion field. + * Currently, only reference transforms qualify. + */ +function getHasPendingCoreMigrationVersionTransform( + doc: SavedObjectUnsanitizedDoc, + migrations: ActiveMigrations, + prop: string +) { + if (!migrations.hasOwnProperty(prop)) { + return false; + } + + const { latestCoreMigrationVersion } = migrations[prop]; + const { coreMigrationVersion } = doc; + return ( + latestCoreMigrationVersion && + (!coreMigrationVersion || Semver.gt(latestCoreMigrationVersion, coreMigrationVersion)) + ); +} + /** * Finds the first unmigrated property in the specified document. */ function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { return props(doc).find((p) => { - const latestVersion = propVersion(migrations, p); + const latestMigrationVersion = propVersion(migrations, p); const docVersion = propVersion(doc, p); - if (latestVersion === docVersion) { - return false; - } - // We verify that the version is not greater than the version supported by Kibana. // If we didn't, this would cause an infinite loop, as we'd be unable to migrate the property // but it would continue to show up as unmigrated. - // If we have a docVersion and the latestVersion is smaller than it or does not exist, + // If we have a docVersion and the latestMigrationVersion is smaller than it or does not exist, // we are dealing with a document that belongs to a future Kibana / plugin version. - if (docVersion && (!latestVersion || Semver.gt(docVersion, latestVersion))) { + if (docVersion && (!latestMigrationVersion || Semver.gt(docVersion, latestMigrationVersion))) { throw Boom.badData( `Document "${doc.id}" has property "${p}" which belongs to a more recent` + - ` version of Kibana [${docVersion}]. The last known version is [${latestVersion}]`, + ` version of Kibana [${docVersion}]. The last known version is [${latestMigrationVersion}]`, doc ); } - return true; + return ( + (latestMigrationVersion && latestMigrationVersion !== docVersion) || + getHasPendingCoreMigrationVersionTransform(doc, migrations, p) // If the object itself is up-to-date, check if its references are up-to-date too + ); }); } @@ -389,23 +737,42 @@ function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMi function migrateProp( doc: SavedObjectUnsanitizedDoc, prop: string, - migrations: ActiveMigrations -): SavedObjectUnsanitizedDoc { + migrations: ActiveMigrations, + convertNamespaceTypes: boolean +): TransformResult { const originalType = doc.type; let migrationVersion = _.clone(doc.migrationVersion) || {}; - const typeChanged = () => !doc.hasOwnProperty(prop) || doc.type !== originalType; + let additionalDocs: SavedObjectUnsanitizedDoc[] = []; - for (const { version, transform } of applicableTransforms(migrations, doc, prop)) { - doc = transform(doc); - migrationVersion = updateMigrationVersion(doc, migrationVersion, prop, version); - doc.migrationVersion = _.clone(migrationVersion); + for (const { version, transform, transformType } of applicableTransforms(migrations, doc, prop)) { + const currentVersion = propVersion(doc, prop); + if (currentVersion && Semver.gt(currentVersion, version)) { + // the previous transform function increased the object's migrationVersion; break out of the loop + break; + } - if (typeChanged()) { + if (convertNamespaceTypes || (transformType !== 'convert' && transformType !== 'reference')) { + // migrate transforms are always applied, but conversion transforms and reference transforms are only applied during index migrations + const result = transform(doc); + doc = result.transformedDoc; + additionalDocs = [...additionalDocs, ...result.additionalDocs]; + } + if (transformType === 'reference') { + // regardless of whether or not the reference transform was applied, update the object's coreMigrationVersion + // this is needed to ensure that we don't have an endless migration loop + doc.coreMigrationVersion = version; + } else { + migrationVersion = updateMigrationVersion(doc, migrationVersion, prop, version); + doc.migrationVersion = _.clone(migrationVersion); + } + + if (doc.type !== originalType) { + // the transform function changed the object's type; break out of the loop break; } } - return doc; + return { transformedDoc: doc, additionalDocs }; } /** @@ -417,9 +784,14 @@ function applicableTransforms( prop: string ) { const minVersion = propVersion(doc, prop); + const minReferenceVersion = doc.coreMigrationVersion || '0.0.0'; const { transforms } = migrations[prop]; return minVersion - ? transforms.filter(({ version }) => Semver.gt(version, minVersion)) + ? transforms.filter(({ version, transformType }) => + transformType === 'reference' + ? Semver.gt(version, minReferenceVersion) + : Semver.gt(version, minVersion) + ) : transforms; } @@ -466,3 +838,14 @@ function assertNoDowngrades( ); } } + +/** + * Deterministically regenerates a saved object's ID based upon it's current namespace, type, and ID. This ensures that we can regenerate + * any existing object IDs without worrying about collisions if two objects that exist in different namespaces share an ID. It also ensures + * that we can later regenerate any inbound object references to match. + * + * @note This is only intended to be used when single-namespace object types are converted into multi-namespace object types. + */ +function deterministicallyRegenerateObjectId(namespace: string, type: string, id: string) { + return uuidv5(`${namespace}:${type}:${id}`, uuidv5.DNS); // the uuidv5 namespace constant (uuidv5.DNS) is arbitrary +} 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 index 025730e71b923..32ecea94826ff 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -557,6 +557,7 @@ describe('ElasticIndex', () => { mappings, count, migrations, + kibanaVersion, }: any) { client.indices.get = jest.fn().mockReturnValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise({ @@ -570,7 +571,12 @@ describe('ElasticIndex', () => { }) ); - const hasMigrations = await Index.migrationsUpToDate(client, index, migrations); + const hasMigrations = await Index.migrationsUpToDate( + client, + index, + migrations, + kibanaVersion + ); return { hasMigrations }; } @@ -584,6 +590,7 @@ describe('ElasticIndex', () => { }, count: 0, migrations: { dashy: '2.3.4' }, + kibanaVersion: '7.10.0', }); expect(hasMigrations).toBeFalsy(); @@ -611,6 +618,7 @@ describe('ElasticIndex', () => { }, count: 2, migrations: {}, + kibanaVersion: '7.10.0', }); expect(hasMigrations).toBeTruthy(); @@ -652,6 +660,7 @@ describe('ElasticIndex', () => { }, count: 3, migrations: { dashy: '23.2.5' }, + kibanaVersion: '7.10.0', }); expect(hasMigrations).toBeFalsy(); @@ -677,6 +686,7 @@ describe('ElasticIndex', () => { bashy: '99.9.3', flashy: '3.4.5', }, + kibanaVersion: '7.10.0', }); function shouldClause(type: string, version: string) { @@ -702,6 +712,15 @@ describe('ElasticIndex', () => { shouldClause('dashy', '23.2.5'), shouldClause('bashy', '99.9.3'), shouldClause('flashy', '3.4.5'), + { + bool: { + must_not: { + term: { + coreMigrationVersion: '7.10.0', + }, + }, + }, + }, ], }, }, diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index c6c00a123295d..9cdec926a56ba 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -147,6 +147,7 @@ export async function migrationsUpToDate( client: MigrationEsClient, index: string, migrationVersion: SavedObjectsMigrationVersion, + kibanaVersion: string, retryCount: number = 10 ): Promise { try { @@ -165,18 +166,29 @@ export async function migrationsUpToDate( body: { query: { bool: { - should: Object.entries(migrationVersion).map(([type, latestVersion]) => ({ - bool: { - must: [ - { exists: { field: type } }, - { - bool: { - must_not: { term: { [`migrationVersion.${type}`]: latestVersion } }, + 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, }, }, - ], + }, }, - })), + ], }, }, }, @@ -194,7 +206,7 @@ export async function migrationsUpToDate( await new Promise((r) => setTimeout(r, 1000)); - return await migrationsUpToDate(client, index, migrationVersion, retryCount - 1); + return await migrationsUpToDate(client, index, migrationVersion, kibanaVersion, retryCount - 1); } } 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 index e82e30ef30031..a8abc75114a96 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -24,6 +24,7 @@ describe('IndexMigrator', () => { batchSize: 10, client: elasticsearchClientMock.createElasticsearchClient(), index: '.kibana', + kibanaVersion: '7.10.0', log: loggingSystemMock.create().get(), mappingProperties: {}, pollInterval: 1, @@ -31,6 +32,7 @@ describe('IndexMigrator', () => { documentMigrator: { migrationVersion: {}, migrate: _.identity, + migrateAndConvert: _.identity, prepareMigrations: jest.fn(), }, serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), @@ -58,6 +60,7 @@ describe('IndexMigrator', () => { namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', + coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', }, @@ -78,6 +81,7 @@ describe('IndexMigrator', () => { id: { type: 'keyword' }, }, }, + coreMigrationVersion: { type: 'keyword' }, }, }, settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, @@ -179,6 +183,7 @@ describe('IndexMigrator', () => { namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', + coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', }, @@ -200,6 +205,7 @@ describe('IndexMigrator', () => { id: { type: 'keyword' }, }, }, + coreMigrationVersion: { type: 'keyword' }, }, }, settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, @@ -240,6 +246,7 @@ describe('IndexMigrator', () => { namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', + coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', }, @@ -261,6 +268,7 @@ describe('IndexMigrator', () => { id: { type: 'keyword' }, }, }, + coreMigrationVersion: { type: 'keyword' }, }, }, settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, @@ -307,17 +315,15 @@ describe('IndexMigrator', () => { test('transforms all docs from the original index', async () => { let count = 0; const { client } = testOpts; - const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { - return { - ...doc, - attributes: { name: ++count }, - }; + 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(), - migrate: migrateDoc, }; withIndex(client, { @@ -331,14 +337,14 @@ describe('IndexMigrator', () => { await new IndexMigrator(testOpts).migrate(); expect(count).toEqual(2); - expect(migrateDoc).toHaveBeenCalledWith({ + expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(1, { id: '1', type: 'foo', attributes: { name: 'Bar' }, migrationVersion: {}, references: [], }); - expect(migrateDoc).toHaveBeenCalledWith({ + expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(2, { id: '2', type: 'foo', attributes: { name: 'Baz' }, @@ -363,14 +369,15 @@ describe('IndexMigrator', () => { test('rejects when the migration function throws an error', async () => { const { client } = testOpts; - const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { + 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(), - migrate: migrateDoc, }; withIndex(client, { diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index bb341e6173aea..869729daab4f3 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -60,13 +60,14 @@ export class IndexMigrator { * Determines what action the migration system needs to take (none, patch, migrate). */ async function requiresMigration(context: Context): Promise { - const { client, alias, documentMigrator, dest, log } = context; + 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 + documentMigrator.migrationVersion, + kibanaVersion ); if (!hasMigrations) { @@ -184,7 +185,7 @@ async function migrateSourceToDest(context: Context) { await Index.write( client, dest.indexName, - await migrateRawDocs(serializer, documentMigrator.migrate, docs, log) + await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs, log) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 4e6c2d0ddfd5c..f3e4b67876b71 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -15,7 +15,9 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { - const transform = jest.fn((doc: any) => set(doc, 'attributes.name', 'HOI!')); + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + ]); const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, @@ -37,14 +39,30 @@ describe('migrateRawDocs', () => { }, ]); - expect(transform).toHaveBeenCalled(); + const obj1 = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + const obj2 = { + id: 'd', + type: 'c', + attributes: { name: 'DDD' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(2); + expect(transform).toHaveBeenNthCalledWith(1, obj1); + expect(transform).toHaveBeenNthCalledWith(2, obj2); }); test('passes invalid docs through untouched and logs error', async () => { const logger = createSavedObjectsMigrationLoggerMock(); - const transform = jest.fn((doc: any) => - set(_.cloneDeep(doc), 'attributes.name', 'TADA') - ); + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'TADA'), + ]); const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, @@ -63,23 +81,53 @@ describe('migrateRawDocs', () => { }, ]); - expect(transform.mock.calls).toEqual([ - [ - { - id: 'd', - type: 'c', - attributes: { - name: 'DDD', - }, - migrationVersion: {}, - references: [], - }, - ], - ]); + const obj2 = { + id: 'd', + type: 'c', + attributes: { name: 'DDD' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(obj2); expect(logger.error).toBeCalledTimes(1); }); + test('handles when one document is transformed into multiple documents', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + { id: 'bar', type: 'foo', attributes: { name: 'baz' } }, + ]); + const result = await migrateRawDocs( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], + createSavedObjectsMigrationLoggerMock() + ); + + expect(result).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'foo:bar', + _source: { type: 'foo', foo: { name: 'baz' }, references: [] }, + }, + ]); + + const obj = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(obj); + }); + test('rejects when the transform function throws an error', async () => { const transform = jest.fn((doc: any) => { throw new Error('error during transform'); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 0f939cd08aff0..fd1b7db36b4eb 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -15,7 +15,7 @@ import { SavedObjectsSerializer, SavedObjectUnsanitizedDoc, } from '../../serialization'; -import { TransformFn } from './document_migrator'; +import { MigrateAndConvertFn } from './document_migrator'; import { SavedObjectsMigrationLogger } from '.'; /** @@ -28,21 +28,24 @@ import { SavedObjectsMigrationLogger } from '.'; */ export async function migrateRawDocs( serializer: SavedObjectsSerializer, - migrateDoc: TransformFn, + migrateDoc: MigrateAndConvertFn, rawDocs: SavedObjectsRawDoc[], log: SavedObjectsMigrationLogger ): Promise { const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc); const processedDocs = []; for (const raw of rawDocs) { - if (serializer.isRawSavedObject(raw)) { - const savedObject = serializer.rawToSavedObject(raw); + const options = { namespaceTreatment: 'lax' as 'lax' }; + if (serializer.isRawSavedObject(raw, options)) { + const savedObject = serializer.rawToSavedObject(raw, options); savedObject.migrationVersion = savedObject.migrationVersion || {}; processedDocs.push( - serializer.savedObjectToRaw({ - references: [], - ...(await migrateDocWithoutBlocking(savedObject)), - }) + ...(await migrateDocWithoutBlocking(savedObject)).map((attrs) => + serializer.savedObjectToRaw({ + references: [], + ...attrs, + }) + ) ); } else { log.error( @@ -63,8 +66,8 @@ export async function migrateRawDocs( * work in between each transform. */ function transformNonBlocking( - transform: TransformFn -): (doc: SavedObjectUnsanitizedDoc) => Promise { + transform: MigrateAndConvertFn +): (doc: SavedObjectUnsanitizedDoc) => Promise { // promises aren't enough to unblock the event loop return (doc: SavedObjectUnsanitizedDoc) => new Promise((resolve, reject) => { diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index a5aaff7df2dd2..62e455c2ddb69 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -32,6 +32,7 @@ export interface MigrationOpts { scrollDuration: string; client: MigrationEsClient; index: string; + kibanaVersion: string; log: Logger; mappingProperties: SavedObjectsTypeMappingDefinitions; documentMigrator: VersionedTransformer; @@ -54,6 +55,7 @@ export interface Context { source: Index.FullIndexInfo; dest: Index.FullIndexInfo; documentMigrator: VersionedTransformer; + kibanaVersion: string; log: SavedObjectsMigrationLogger; batchSize: number; pollInterval: number; @@ -78,6 +80,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { alias, source, dest, + kibanaVersion: opts.kibanaVersion, log: new MigrationLogger(log), batchSize: opts.batchSize, documentMigrator: opts.documentMigrator, diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 9311292a6a0ed..32c2536ab0296 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -6,6 +6,7 @@ Object { "migrationMappingPropertyHashes": Object { "amap": "510f1f0adb69830cf8a1c5ce2923ed82", "bmap": "510f1f0adb69830cf8a1c5ce2923ed82", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", @@ -31,6 +32,9 @@ Object { }, }, }, + "coreMigrationVersion": Object { + "type": "keyword", + }, "migrationVersion": Object { "dynamic": "true", "type": "object", diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index aea29479d2af0..c8bc4b2e14123 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -90,6 +90,7 @@ export class KibanaMigrator { }: KibanaMigratorOptions) { this.client = client; this.kibanaConfig = kibanaConfig; + this.kibanaVersion = kibanaVersion; this.savedObjectsConfig = savedObjectsConfig; this.typeRegistry = typeRegistry; this.serializer = new SavedObjectsSerializer(this.typeRegistry); @@ -177,7 +178,7 @@ export class KibanaMigrator { transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => migrateRawDocs( this.serializer, - this.documentMigrator.migrate, + this.documentMigrator.migrateAndConvert, rawDocs, new MigrationLogger(this.log) ), @@ -192,6 +193,7 @@ export class KibanaMigrator { client: createMigrationEsClient(this.client, this.log, this.migrationsRetryDelay), documentMigrator: this.documentMigrator, index, + kibanaVersion: this.kibanaVersion, log: this.log, mappingProperties: indexMap[index].typeMappings, pollInterval: this.savedObjectsConfig.pollInterval, diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index b54d0222a1478..52b4f50d599d9 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -61,7 +61,7 @@ export interface SavedObjectMigrationContext { /** * A map of {@link SavedObjectMigrationFn | migration functions} to be used for a given type. - * The map's keys must be valid semver versions. + * The map's keys must be valid semver versions, and they cannot exceed the current Kibana version. * * For a given document, only migrations with a higher version number than that of the document will be applied. * Migrations are executed in order, starting from the lowest version and ending with the highest one. diff --git a/src/core/server/saved_objects/object_types/constants.ts b/src/core/server/saved_objects/object_types/constants.ts new file mode 100644 index 0000000000000..4e05c406c653f --- /dev/null +++ b/src/core/server/saved_objects/object_types/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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * @internal + */ +export const LEGACY_URL_ALIAS_TYPE = 'legacy-url-alias'; diff --git a/src/core/server/saved_objects/object_types/index.ts b/src/core/server/saved_objects/object_types/index.ts new file mode 100644 index 0000000000000..1a9bccdc17c28 --- /dev/null +++ b/src/core/server/saved_objects/object_types/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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { LEGACY_URL_ALIAS_TYPE } from './constants'; +export { LegacyUrlAlias } from './types'; +export { registerCoreObjectTypes } from './registration'; diff --git a/src/core/server/saved_objects/object_types/registration.test.ts b/src/core/server/saved_objects/object_types/registration.test.ts new file mode 100644 index 0000000000000..9bd7b3d61e099 --- /dev/null +++ b/src/core/server/saved_objects/object_types/registration.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { LEGACY_URL_ALIAS_TYPE } from './constants'; +import { registerCoreObjectTypes } from './registration'; + +describe('Core saved object types registration', () => { + describe('#registerCoreObjectTypes', () => { + it('registers all expected types', () => { + const typeRegistry = typeRegistryMock.create(); + registerCoreObjectTypes(typeRegistry); + + expect(typeRegistry.registerType).toHaveBeenCalledTimes(1); + expect(typeRegistry.registerType).toHaveBeenCalledWith( + expect.objectContaining({ name: LEGACY_URL_ALIAS_TYPE }) + ); + }); + }); +}); diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts new file mode 100644 index 0000000000000..82562ac53a109 --- /dev/null +++ b/src/core/server/saved_objects/object_types/registration.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { LEGACY_URL_ALIAS_TYPE } from './constants'; +import { ISavedObjectTypeRegistry, SavedObjectsType, SavedObjectTypeRegistry } from '..'; + +const legacyUrlAliasType: SavedObjectsType = { + name: LEGACY_URL_ALIAS_TYPE, + namespaceType: 'agnostic', + mappings: { + dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields + properties: {}, + }, + hidden: true, +}; + +/** + * @internal + */ +export function registerCoreObjectTypes( + typeRegistry: ISavedObjectTypeRegistry & Pick +) { + typeRegistry.registerType(legacyUrlAliasType); +} diff --git a/src/core/server/saved_objects/object_types/types.ts b/src/core/server/saved_objects/object_types/types.ts new file mode 100644 index 0000000000000..8391311cbefdf --- /dev/null +++ b/src/core/server/saved_objects/object_types/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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** + * @internal + */ +export interface LegacyUrlAlias { + targetNamespace: string; + targetType: string; + targetId: string; + lastResolved?: string; + resolveCounter?: number; + disabled?: boolean; +} diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index eee9b9b1a0bff..6d57eaa3777e6 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -29,6 +29,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout attributes: schema.recordOf(schema.string(), schema.any()), version: schema.maybe(schema.string()), migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())), + coreMigrationVersion: schema.maybe(schema.string()), references: schema.maybe( schema.arrayOf( schema.object({ diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index e486580320da9..fd256abac3526 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -29,6 +29,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep body: schema.object({ attributes: schema.recordOf(schema.string(), schema.any()), migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())), + coreMigrationVersion: schema.maybe(schema.string()), references: schema.maybe( schema.arrayOf( schema.object({ @@ -45,12 +46,25 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep router.handleLegacyErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; - const { attributes, migrationVersion, references, initialNamespaces } = req.body; + const { + attributes, + migrationVersion, + coreMigrationVersion, + references, + initialNamespaces, + } = req.body; const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {}); - const options = { id, overwrite, migrationVersion, references, initialNamespaces }; + const options = { + id, + overwrite, + migrationVersion, + coreMigrationVersion, + references, + initialNamespaces, + }; const result = await context.core.savedObjects.client.create(type, attributes, options); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 67a4e305e87ad..412dd0e7ffbc0 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -12,6 +12,7 @@ import { Logger } from '../../logging'; import { SavedObjectConfig } from '../saved_objects_config'; import { IKibanaMigrator } from '../migrations'; import { registerGetRoute } from './get'; +import { registerResolveRoute } from './resolve'; import { registerCreateRoute } from './create'; import { registerDeleteRoute } from './delete'; import { registerFindRoute } from './find'; @@ -41,6 +42,7 @@ export function registerRoutes({ const router = http.createRouter('/api/saved_objects/'); registerGetRoute(router, { coreUsageData }); + registerResolveRoute(router, { coreUsageData }); registerCreateRoute(router, { coreUsageData }); registerDeleteRoute(router, { coreUsageData }); registerFindRoute(router, { coreUsageData }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts new file mode 100644 index 0000000000000..5ddeb29b8c2d5 --- /dev/null +++ b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import supertest from 'supertest'; +import { registerResolveRoute } from '../resolve'; +import { ContextService } from '../../../context'; +import { savedObjectsClientMock } from '../../service/saved_objects_client.mock'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; +import { HttpService, InternalHttpServiceSetup } from '../../../http'; +import { createHttpServer, createCoreContext } from '../../../http/test_utils'; +import { coreMock } from '../../../mocks'; + +const coreId = Symbol('core'); + +describe('GET /api/saved_objects/resolve/{type}/{id}', () => { + let server: HttpService; + let httpSetup: InternalHttpServiceSetup; + let handlerContext: ReturnType; + let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; + + beforeEach(async () => { + const coreContext = createCoreContext({ coreId }); + server = createHttpServer(coreContext); + + const contextService = new ContextService(coreContext); + httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + + handlerContext = coreMock.createRequestHandlerContext(); + savedObjectsClient = handlerContext.savedObjects.client; + + httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => { + return handlerContext; + }); + + const router = httpSetup.createRouter('/api/saved_objects/'); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerResolveRoute(router, { coreUsageData }); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('formats successful response', async () => { + const clientResponse = { + saved_object: { + id: 'logstash-*', + title: 'logstash-*', + type: 'logstash-type', + attributes: {}, + timeFieldName: '@timestamp', + notExpandable: true, + references: [], + }, + outcome: 'exactMatch' as 'exactMatch', + }; + + savedObjectsClient.resolve.mockResolvedValue(clientResponse); + + const result = await supertest(httpSetup.server.listener) + .get('/api/saved_objects/resolve/index-pattern/logstash-*') + .expect(200); + + expect(result.body).toEqual(clientResponse); + }); + + it('calls upon savedObjectClient.resolve', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/resolve/index-pattern/logstash-*') + .expect(200); + + expect(savedObjectsClient.resolve).toHaveBeenCalled(); + + const args = savedObjectsClient.resolve.mock.calls[0]; + expect(args).toEqual(['index-pattern', 'logstash-*']); + }); +}); diff --git a/src/core/server/saved_objects/routes/resolve.ts b/src/core/server/saved_objects/routes/resolve.ts new file mode 100644 index 0000000000000..28a3f4b876467 --- /dev/null +++ b/src/core/server/saved_objects/routes/resolve.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; + +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { + router.get( + { + path: '/resolve/{type}/{id}', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { type, id } = req.params; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsResolve({ request: req }).catch(() => {}); + + const result = await context.core.savedObjects.client.resolve(type, id); + return res.ok({ body: result }); + }) + ); +}; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 3c1e217dcc229..4a8caa7686606 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -25,8 +25,10 @@ import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; import { SavedObjectsRepository } from './service/lib/repository'; +import { registerCoreObjectTypes } from './object_types'; jest.mock('./service/lib/repository'); +jest.mock('./object_types'); describe('SavedObjectsService', () => { const createCoreContext = ({ @@ -67,6 +69,16 @@ describe('SavedObjectsService', () => { }); describe('#setup()', () => { + it('calls registerCoreObjectTypes', async () => { + const coreContext = createCoreContext(); + const soService = new SavedObjectsService(coreContext); + + const mockedRegisterCoreObjectTypes = registerCoreObjectTypes as jest.Mock; + expect(mockedRegisterCoreObjectTypes).not.toHaveBeenCalled(); + await soService.setup(createSetupDeps()); + expect(mockedRegisterCoreObjectTypes).toHaveBeenCalledTimes(1); + }); + describe('#setClientFactoryProvider', () => { it('registers the factory to the clientProvider', async () => { const coreContext = createCoreContext(); @@ -130,6 +142,7 @@ describe('SavedObjectsService', () => { describe('#registerType', () => { it('registers the type to the internal typeRegistry', async () => { + // we mocked registerCoreObjectTypes above, so this test case only reflects direct calls to the registerType method const coreContext = createCoreContext(); const soService = new SavedObjectsService(coreContext); const setup = await soService.setup(createSetupDeps()); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 6db4cf4f781b4..40c8c576b0eca 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -43,6 +43,7 @@ import { SavedObjectsImporter, ISavedObjectsImporter } from './import'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; +import { registerCoreObjectTypes } from './object_types'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -305,6 +306,8 @@ export class SavedObjectsService migratorPromise: this.migrator$.pipe(first()).toPromise(), }); + registerCoreObjectTypes(this.typeRegistry); + return { status$: calculateStatus$( this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())), diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index ba6115dbff3ae..3ffaf9cf1e7c8 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -15,6 +15,7 @@ export { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, SavedObjectsRawDoc, + SavedObjectsRawDocParseOptions, SavedObjectsRawDocSource, } from './types'; export { SavedObjectsSerializer } from './serializer'; diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 4d0527aee01bc..b09fb1ab30c79 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -11,6 +11,7 @@ import { SavedObjectsSerializer } from './serializer'; import { SavedObjectsRawDoc } from './types'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { encodeVersion } from '../version'; +import { LEGACY_URL_ALIAS_TYPE } from '../object_types'; let typeRegistry = typeRegistryMock.create(); typeRegistry.isNamespaceAgnostic.mockReturnValue(true); @@ -131,6 +132,27 @@ describe('#rawToSavedObject', () => { expect(expected).toEqual(actual); }); + test('if specified it copies the _source.coreMigrationVersion property to coreMigrationVersion', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + coreMigrationVersion: '1.2.3', + }, + }); + expect(actual).toHaveProperty('coreMigrationVersion', '1.2.3'); + }); + + test(`if _source.coreMigrationVersion is unspecified it doesn't set coreMigrationVersion`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, + }); + expect(actual).not.toHaveProperty('coreMigrationVersion'); + }); + test(`if version is unspecified it doesn't set version`, () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', @@ -288,6 +310,7 @@ describe('#rawToSavedObject', () => { foo: '1.2.3', bar: '9.8.7', }, + coreMigrationVersion: '4.5.6', namespace: 'foo-namespace', updated_at: String(new Date()), references: [], @@ -412,6 +435,41 @@ describe('#rawToSavedObject', () => { test(`doesn't copy _source.namespace to namespace`, () => { expect(actual).not.toHaveProperty('namespace'); }); + + describe('with lax namespaceTreatment', () => { + const options = { namespaceTreatment: 'lax' as 'lax' }; + + test(`removes type prefix from _id and, and does not copy _source.namespace to namespace`, () => { + const _actual = multiNamespaceSerializer.rawToSavedObject(raw, options); + expect(_actual).toHaveProperty('id', 'bar'); + expect(_actual).not.toHaveProperty('namespace'); + }); + + test(`removes type and namespace prefix from _id, and copies _source.namespace to namespace`, () => { + const _id = `${raw._source.namespace}:${raw._id}`; + const _actual = multiNamespaceSerializer.rawToSavedObject({ ...raw, _id }, options); + expect(_actual).toHaveProperty('id', 'bar'); + expect(_actual).toHaveProperty('namespace', raw._source.namespace); // "baz" + }); + + test(`removes type and namespace prefix from _id when the namespace matches the type`, () => { + const _raw = createSampleDoc({ _id: 'foo:foo:bar', _source: { namespace: 'foo' } }); + const _actual = multiNamespaceSerializer.rawToSavedObject(_raw, options); + expect(_actual).toHaveProperty('id', 'bar'); + expect(_actual).toHaveProperty('namespace', 'foo'); + }); + + test(`does not remove the entire _id when the namespace matches the type`, () => { + // This is not a realistic/valid document, but we defensively check to ensure we aren't trimming the entire ID. + // In this test case, a multi-namespace document has a raw ID with the type prefix "foo:" and an object ID of "foo:" (no namespace + // prefix). This document *also* has a `namespace` field the same as the type, while it should not have a `namespace` field at all + // since it has no namespace prefix in its raw ID. + const _raw = createSampleDoc({ _id: 'foo:foo:', _source: { namespace: 'foo' } }); + const _actual = multiNamespaceSerializer.rawToSavedObject(_raw, options); + expect(_actual).toHaveProperty('id', 'foo:'); + expect(_actual).not.toHaveProperty('namespace'); + }); + }); }); describe('multi-namespace type with namespaces', () => { @@ -515,6 +573,25 @@ describe('#savedObjectToRaw', () => { expect(actual._source).not.toHaveProperty('migrationVersion'); }); + test('it copies the coreMigrationVersion property to _source.coreMigrationVersion', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + coreMigrationVersion: '1.2.3', + } as any); + + expect(actual._source).toHaveProperty('coreMigrationVersion', '1.2.3'); + }); + + test(`if unspecified it doesn't add coreMigrationVersion property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual._source).not.toHaveProperty('coreMigrationVersion'); + }); + test('it decodes the version property to _seq_no and _primary_term', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', @@ -841,6 +918,116 @@ describe('#isRawSavedObject', () => { }); }); + describe('multi-namespace type with a namespace', () => { + test('is true if the id is prefixed with type and the type matches', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeTruthy(); + }); + + test('is false if the id is not prefixed', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the id is prefixed with type and namespace', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is true if the id is prefixed with type and namespace, and namespaceTreatment is lax', () => { + const options = { namespaceTreatment: 'lax' as 'lax' }; + expect( + multiNamespaceSerializer.isRawSavedObject( + { + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }, + options + ) + ).toBeTruthy(); + }); + + test(`is false if the type prefix omits the :`, () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'helloworld', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute is missing', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + hello: {}, + namespace: 'foo', + } as any, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute does not match the id', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if there is no [type] attribute', () => { + expect( + multiNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + }); + describe('namespace-agnostic type with a namespace', () => { test('is true if the id is prefixed with type and the type matches', () => { expect( @@ -950,6 +1137,13 @@ describe('#generateRawId', () => { }); }); + describe('multi-namespace type with a namespace', () => { + test(`uses the id that is specified and doesn't prefix the namespace`, () => { + const id = multiNamespaceSerializer.generateRawId('foo', 'hello', 'world'); + expect(id).toEqual('hello:world'); + }); + }); + describe('namespace-agnostic type with a namespace', () => { test(`uses the id that is specified and doesn't prefix the namespace`, () => { const id = namespaceAgnosticSerializer.generateRawId('foo', 'hello', 'world'); @@ -957,3 +1151,24 @@ describe('#generateRawId', () => { }); }); }); + +describe('#generateRawLegacyUrlAliasId', () => { + describe(`returns expected value`, () => { + const expected = `${LEGACY_URL_ALIAS_TYPE}:foo:bar:baz`; + + test(`for single-namespace types`, () => { + const id = singleNamespaceSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz'); + expect(id).toEqual(expected); + }); + + test(`for multi-namespace types`, () => { + const id = multiNamespaceSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz'); + expect(id).toEqual(expected); + }); + + test(`for namespace-agnostic types`, () => { + const id = namespaceAgnosticSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz'); + expect(id).toEqual(expected); + }); + }); +}); diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 7a1de0ed2c960..4e9c3b6be03cf 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -6,9 +6,14 @@ * Public License, v 1. */ +import { LEGACY_URL_ALIAS_TYPE } from '../object_types'; import { decodeVersion, encodeVersion } from '../version'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; -import { SavedObjectsRawDoc, SavedObjectSanitizedDoc } from './types'; +import { + SavedObjectsRawDoc, + SavedObjectSanitizedDoc, + SavedObjectsRawDocParseOptions, +} from './types'; /** * A serializer that can be used to manually convert {@link SavedObjectsRawDoc | raw} or @@ -30,42 +35,60 @@ export class SavedObjectsSerializer { /** * Determines whether or not the raw document can be converted to a saved object. * - * @param {SavedObjectsRawDoc} rawDoc - The raw ES document to be tested + * @param {SavedObjectsRawDoc} doc - The raw ES document to be tested + * @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document. */ - public isRawSavedObject(rawDoc: SavedObjectsRawDoc) { - const { type, namespace } = rawDoc._source; - const namespacePrefix = - namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; - return Boolean( - type && - rawDoc._id.startsWith(`${namespacePrefix}${type}:`) && - rawDoc._source.hasOwnProperty(type) - ); + public isRawSavedObject(doc: SavedObjectsRawDoc, options: SavedObjectsRawDocParseOptions = {}) { + const { namespaceTreatment = 'strict' } = options; + const { _id, _source } = doc; + const { type, namespace } = _source; + if (!type) { + return false; + } + const { idMatchesPrefix } = this.parseIdPrefix(namespace, type, _id, namespaceTreatment); + return idMatchesPrefix && _source.hasOwnProperty(type); } /** * Converts a document from the format that is stored in elasticsearch to the saved object client format. * - * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. + * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. + * @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document. */ - public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { + public rawToSavedObject( + doc: SavedObjectsRawDoc, + options: SavedObjectsRawDocParseOptions = {} + ): SavedObjectSanitizedDoc { + const { namespaceTreatment = 'strict' } = options; const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace, namespaces, originId } = _source; + const { + type, + namespaces, + originId, + migrationVersion, + references, + coreMigrationVersion, + } = _source; const version = _seq_no != null || _primary_term != null ? encodeVersion(_seq_no!, _primary_term!) : undefined; + const { id, namespace } = this.trimIdPrefix(_source.namespace, type, _id, namespaceTreatment); + const includeNamespace = + namespace && (namespaceTreatment === 'lax' || this.registry.isSingleNamespace(type)); + const includeNamespaces = this.registry.isMultiNamespace(type); return { type, - id: this.trimIdPrefix(namespace, type, _id), - ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), - ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), + id, + ...(includeNamespace && { namespace }), + ...(includeNamespaces && { namespaces }), ...(originId && { originId }), attributes: _source[type], - references: _source.references || [], - ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), + references: references || [], + ...(migrationVersion && { migrationVersion }), + ...(coreMigrationVersion && { coreMigrationVersion }), ...(_source.updated_at && { updated_at: _source.updated_at }), ...(version && { version }), }; @@ -89,6 +112,7 @@ export class SavedObjectsSerializer { updated_at, version, references, + coreMigrationVersion, } = savedObj; const source = { [type]: attributes, @@ -98,6 +122,7 @@ export class SavedObjectsSerializer { ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), ...(originId && { originId }), ...(migrationVersion && { migrationVersion }), + ...(coreMigrationVersion && { coreMigrationVersion }), ...(updated_at && { updated_at }), }; @@ -121,22 +146,77 @@ export class SavedObjectsSerializer { return `${namespacePrefix}${type}:${id}`; } - private trimIdPrefix(namespace: string | undefined, type: string, id: string) { + /** + * Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias. + * + * @param {string} namespace - The namespace of the saved object + * @param {string} type - The saved object type + * @param {string} id - The id of the saved object + */ + public generateRawLegacyUrlAliasId(namespace: string, type: string, id: string) { + return `${LEGACY_URL_ALIAS_TYPE}:${namespace}:${type}:${id}`; + } + + /** + * Given a document's source namespace, type, and raw ID, trim the ID prefix (based on the namespaceType), returning the object ID and the + * detected namespace. A single-namespace object is only considered to exist in a namespace if its raw ID is prefixed by that *and* it has + * the namespace field in its source. + */ + private trimIdPrefix( + sourceNamespace: string | undefined, + type: string, + id: string, + namespaceTreatment: 'strict' | 'lax' + ) { assertNonEmptyString(id, 'document id'); assertNonEmptyString(type, 'saved object type'); - const namespacePrefix = - namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; - const prefix = `${namespacePrefix}${type}:`; + const { prefix, idMatchesPrefix, namespace } = this.parseIdPrefix( + sourceNamespace, + type, + id, + namespaceTreatment + ); + return { + id: idMatchesPrefix ? id.slice(prefix.length) : id, + namespace, + }; + } - if (!id.startsWith(prefix)) { - return id; + private parseIdPrefix( + sourceNamespace: string | undefined, + type: string, + id: string, + namespaceTreatment: 'strict' | 'lax' + ) { + let prefix: string; // the prefix that is used to validate this raw object ID + let namespace: string | undefined; // the namespace that is in the raw object ID (only for single-namespace objects) + const parseFlexibly = namespaceTreatment === 'lax' && this.registry.isMultiNamespace(type); + if (sourceNamespace && (this.registry.isSingleNamespace(type) || parseFlexibly)) { + prefix = `${sourceNamespace}:${type}:`; + if (parseFlexibly && !checkIdMatchesPrefix(id, prefix)) { + prefix = `${type}:`; + } else { + // this is either a single-namespace object, or is being converted into a multi-namespace object + namespace = sourceNamespace; + } + } else { + // there is no source namespace, OR there is a source namespace but this is not a single-namespace object + prefix = `${type}:`; } - return id.slice(prefix.length); + return { + prefix, + idMatchesPrefix: checkIdMatchesPrefix(id, prefix), + namespace, + }; } } +function checkIdMatchesPrefix(id: string, prefix: string) { + return id.startsWith(prefix) && id.length > prefix.length; +} + function assertNonEmptyString(value: string, name: string) { if (!value || typeof value !== 'string') { throw new TypeError(`Expected "${value}" to be a ${name}`); diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 95deedbb7d9c0..5de168a08f1db 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -43,6 +43,7 @@ interface SavedObjectDoc { namespace?: string; namespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; + coreMigrationVersion?: string; version?: string; updated_at?: string; originId?: string; @@ -68,3 +69,19 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial * @public */ export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; + +/** + * Options that can be specified when using the saved objects serializer to parse a raw document. + * + * @public + */ +export interface SavedObjectsRawDocParseOptions { + /** + * Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously + * single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade + * migrations. + * + * If not specified, the default treatment is `strict`. + */ + namespaceTreatment?: 'strict' | 'lax'; +} diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 6b00816e4c17b..3f2a2d677c42d 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -8,7 +8,7 @@ import { includedFields } from './included_fields'; -const BASE_FIELD_COUNT = 9; +const BASE_FIELD_COUNT = 10; describe('includedFields', () => { it('returns undefined if fields are not provided', () => { @@ -32,6 +32,7 @@ Array [ "type", "references", "migrationVersion", + "coreMigrationVersion", "updated_at", "originId", "foo", @@ -66,6 +67,7 @@ Array [ "type", "references", "migrationVersion", + "coreMigrationVersion", "updated_at", "originId", "foo", diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index 3e11d8d8ad4ef..7aaca2caf003f 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -30,6 +30,7 @@ export function includedFields(type: string | string[] = '*', fields?: string[] .concat('type') .concat('references') .concat('migrationVersion') + .concat('coreMigrationVersion') .concat('updated_at') .concat('originId') .concat(fields); // v5 compatibility diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index f5b7d30aee4fd..93e73f3255b87 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -17,6 +17,7 @@ const create = (): jest.Mocked => ({ bulkGet: jest.fn(), find: jest.fn(), get: jest.fn(), + resolve: jest.fn(), update: jest.fn(), addToNamespaces: jest.fn(), deleteFromNamespaces: jest.fn(), diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index d8cdec1e0b8a5..216e1c4bd2d3c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -13,6 +13,7 @@ import { ALL_NAMESPACES_STRING } from './utils'; import { SavedObjectsSerializer } from '../../serialization'; import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; @@ -44,6 +45,7 @@ describe('SavedObjectsRepository', () => { const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; const mockVersion = encodeHitVersion(mockVersionProps); + const KIBANA_VERSION = '2.0.0'; const CUSTOM_INDEX_TYPE = 'customIndex'; const NAMESPACE_AGNOSTIC_TYPE = 'globalType'; const MULTI_NAMESPACE_TYPE = 'shareableType'; @@ -142,7 +144,7 @@ describe('SavedObjectsRepository', () => { const documentMigrator = new DocumentMigrator({ typeRegistry: registry, - kibanaVersion: '2.0.0', + kibanaVersion: KIBANA_VERSION, log: {}, }); @@ -216,6 +218,7 @@ describe('SavedObjectsRepository', () => { rawToSavedObject: jest.fn(), savedObjectToRaw: jest.fn(), generateRawId: jest.fn(), + generateRawLegacyUrlAliasId: jest.fn(), trimIdPrefix: jest.fn(), }; const _serializer = new SavedObjectsSerializer(registry); @@ -501,6 +504,7 @@ describe('SavedObjectsRepository', () => { const expectSuccessResult = (obj) => ({ ...obj, migrationVersion: { [obj.type]: '1.1.1' }, + coreMigrationVersion: KIBANA_VERSION, version: mockVersion, namespaces: obj.namespaces ?? [obj.namespace ?? 'default'], ...mockTimestampFields, @@ -954,6 +958,7 @@ describe('SavedObjectsRepository', () => { ...response.items[0].create, _source: { ...response.items[0].create._source, + coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation namespaces: response.items[0].create._source.namespaces, }, _id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/), @@ -962,6 +967,7 @@ describe('SavedObjectsRepository', () => { ...response.items[1].create, _source: { ...response.items[1].create._source, + coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation namespaces: response.items[1].create._source.namespaces, }, }); @@ -2140,6 +2146,7 @@ describe('SavedObjectsRepository', () => { references, namespaces: [namespace ?? 'default'], migrationVersion: { [type]: '1.1.1' }, + coreMigrationVersion: KIBANA_VERSION, }); }); }); @@ -2724,6 +2731,7 @@ describe('SavedObjectsRepository', () => { 'type', 'references', 'migrationVersion', + 'coreMigrationVersion', 'updated_at', 'originId', 'title', @@ -3254,6 +3262,231 @@ describe('SavedObjectsRepository', () => { }); }); + describe('#resolve', () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + const aliasTargetId = 'some-other-id'; // only used for 'aliasMatch' and 'conflict' outcomes + const namespace = 'foo-namespace'; + + const getMockAliasDocument = (resolveCounter) => ({ + body: { + get: { + _source: { + [LEGACY_URL_ALIAS_TYPE]: { + targetId: aliasTargetId, + ...(resolveCounter && { resolveCounter }), + // other fields are not used by the repository + }, + }, + }, + }, + }); + + describe('outcomes', () => { + describe('error', () => { + const expectNotFoundError = async (type, id, options) => { + await expect(savedObjectsRepository.resolve(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it('because type is invalid', async () => { + await expectNotFoundError('unknownType', id); + expect(client.update).not.toHaveBeenCalled(); + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).not.toHaveBeenCalled(); + }); + + it('because type is hidden', async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(client.update).not.toHaveBeenCalled(); + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).not.toHaveBeenCalled(); + }); + + it('because alias is not used and actual object is not found', async () => { + const options = { namespace: undefined }; + const response = { found: false }; + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + ); + + await expectNotFoundError(type, id, options); + expect(client.update).not.toHaveBeenCalled(); + expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target + expect(client.mget).not.toHaveBeenCalled(); + }); + + it('because actual object and alias object are both not found', async () => { + const options = { namespace }; + const objectResults = [ + { type, id, found: false }, + { type, id: aliasTargetId, found: false }, + ]; + client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object + const response = getMockMgetResponse(objectResults, options.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + ); + + await expectNotFoundError(type, id, options); + expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target + }); + }); + + describe('exactMatch', () => { + it('because namespace is undefined', async () => { + const options = { namespace: undefined }; + const response = getMockGetResponse({ type, id }); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + ); + + const result = await savedObjectsRepository.resolve(type, id, options); + expect(client.update).not.toHaveBeenCalled(); + expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target + expect(client.mget).not.toHaveBeenCalled(); + expect(result).toEqual({ + saved_object: expect.objectContaining({ type, id }), + outcome: 'exactMatch', + }); + }); + + describe('because alias is not used', () => { + const expectExactMatchResult = async (aliasResult) => { + const options = { namespace }; + client.update.mockResolvedValueOnce(aliasResult); // for alias object + const response = getMockGetResponse({ type, id }, options.namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + ); + + const result = await savedObjectsRepository.resolve(type, id, options); + expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object + expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target + expect(client.mget).not.toHaveBeenCalled(); + expect(result).toEqual({ + saved_object: expect.objectContaining({ type, id }), + outcome: 'exactMatch', + }); + }; + + it('since alias call resulted in 404', async () => { + await expectExactMatchResult({ statusCode: 404 }); + }); + + it('since alias is not found', async () => { + await expectExactMatchResult({ body: { get: { found: false } } }); + }); + + it('since alias is disabled', async () => { + await expectExactMatchResult({ + body: { get: { _source: { [LEGACY_URL_ALIAS_TYPE]: { disabled: true } } } }, + }); + }); + }); + + describe('because alias is used', () => { + const expectExactMatchResult = async (objectResults) => { + const options = { namespace }; + client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object + const response = getMockMgetResponse(objectResults, options.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target + ); + + const result = await savedObjectsRepository.resolve(type, id, options); + expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target + expect(result).toEqual({ + saved_object: expect.objectContaining({ type, id }), + outcome: 'exactMatch', + }); + }; + + it('but alias target is not found', async () => { + const objects = [ + { type, id }, + { type, id: aliasTargetId, found: false }, + ]; + await expectExactMatchResult(objects); + }); + + it('but alias target does not exist in this namespace', async () => { + const objects = [ + { type: MULTI_NAMESPACE_TYPE, id }, // correct namespace field is added by getMockMgetResponse + { type: MULTI_NAMESPACE_TYPE, id: aliasTargetId, namespace: `not-${namespace}` }, // overrides namespace field that would otherwise be added by getMockMgetResponse + ]; + await expectExactMatchResult(objects); + }); + }); + }); + + describe('aliasMatch', () => { + const expectAliasMatchResult = async (objectResults) => { + const options = { namespace }; + client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object + const response = getMockMgetResponse(objectResults, options.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target + ); + + const result = await savedObjectsRepository.resolve(type, id, options); + expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target + expect(result).toEqual({ + saved_object: expect.objectContaining({ type, id: aliasTargetId }), + outcome: 'aliasMatch', + }); + }; + + it('because actual target is not found', async () => { + const objects = [ + { type, id, found: false }, + { type, id: aliasTargetId }, + ]; + await expectAliasMatchResult(objects); + }); + + it('because actual target does not exist in this namespace', async () => { + const objects = [ + { type: MULTI_NAMESPACE_TYPE, id, namespace: `not-${namespace}` }, // overrides namespace field that would otherwise be added by getMockMgetResponse + { type: MULTI_NAMESPACE_TYPE, id: aliasTargetId }, // correct namespace field is added by getMockMgetResponse + ]; + await expectAliasMatchResult(objects); + }); + }); + + describe('conflict', () => { + it('because actual target and alias target are both found', async () => { + const options = { namespace }; + const objectResults = [ + { type, id }, // correct namespace field is added by getMockMgetResponse + { type, id: aliasTargetId }, // correct namespace field is added by getMockMgetResponse + ]; + client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object + const response = getMockMgetResponse(objectResults, options.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target + ); + + const result = await savedObjectsRepository.resolve(type, id, options); + expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object + expect(client.get).not.toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target + expect(result).toEqual({ + saved_object: expect.objectContaining({ type, id }), + outcome: 'conflict', + }); + }); + }); + }); + }); + describe('#incrementCounter', () => { const type = 'config'; const id = 'one'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 685760e81a2b7..2993d4234bd2e 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -47,6 +47,7 @@ import { SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsResolveResponse, } from '../saved_objects_client'; import { SavedObject, @@ -55,6 +56,7 @@ import { SavedObjectsMigrationVersion, MutatingOperationRefreshSetting, } from '../../types'; +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; import { @@ -920,25 +922,7 @@ export class SavedObjectsRepository { } as any) as SavedObject; } - const { originId, updated_at: updatedAt } = doc._source; - let namespaces = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(doc._source.namespace), - ]; - } - - return { - id, - type, - namespaces, - ...(originId && { originId }), - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - }; + return this.getSavedObjectFromSource(type, id, doc); }), }; } @@ -978,26 +962,122 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { originId, updated_at: updatedAt } = body._source; + return this.getSavedObjectFromSource(type, id, body); + } - let namespaces: string[] = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(body._source.namespace), - ]; + /** + * Resolves a single object, using any legacy URL alias if it exists + * + * @param {string} type + * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] + * @returns {promise} - { saved_object, outcome } + */ + async resolve( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> { + if (!this._allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return { - id, - type, - namespaces, - ...(originId && { originId }), - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(body), - attributes: body._source[type], - references: body._source.references || [], - migrationVersion: body._source.migrationVersion, - }; + const namespace = normalizeNamespace(options.namespace); + if (namespace === undefined) { + // legacy URL aliases cannot exist for the default namespace; just attempt to get the object + return this.resolveExactMatch(type, id, options); + } + + const rawAliasId = this._serializer.generateRawLegacyUrlAliasId(namespace, type, id); + const time = this._getCurrentTime(); + + // retrieve the alias, and if it is not disabled, update it + const aliasResponse = await this.client.update( + { + id: rawAliasId, + index: this.getIndexForType(LEGACY_URL_ALIAS_TYPE), + refresh: false, + _source: 'true', + body: { + script: { + source: ` + if (ctx._source[params.type].disabled != true) { + if (ctx._source[params.type].resolveCounter == null) { + ctx._source[params.type].resolveCounter = 1; + } + else { + ctx._source[params.type].resolveCounter += 1; + } + ctx._source[params.type].lastResolved = params.time; + ctx._source.updated_at = params.time; + } + `, + lang: 'painless', + params: { + type: LEGACY_URL_ALIAS_TYPE, + time, + }, + }, + }, + }, + { ignore: [404] } + ); + + if ( + aliasResponse.statusCode === 404 || + aliasResponse.body.get.found === false || + aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE]?.disabled === true + ) { + // no legacy URL alias exists, or one exists but it's disabled; just attempt to get the object + return this.resolveExactMatch(type, id, options); + } + const legacyUrlAlias: LegacyUrlAlias = aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE]; + const objectIndex = this.getIndexForType(type); + const bulkGetResponse = await this.client.mget( + { + body: { + docs: [ + { + // attempt to find an exact match for the given ID + _id: this._serializer.generateRawId(namespace, type, id), + _index: objectIndex, + }, + { + // also attempt to find a match for the legacy URL alias target ID + _id: this._serializer.generateRawId(namespace, type, legacyUrlAlias.targetId), + _index: objectIndex, + }, + ], + }, + }, + { ignore: [404] } + ); + + const exactMatchDoc = bulkGetResponse?.body.docs[0]; + const aliasMatchDoc = bulkGetResponse?.body.docs[1]; + const foundExactMatch = + exactMatchDoc.found && this.rawDocExistsInNamespace(exactMatchDoc, namespace); + const foundAliasMatch = + aliasMatchDoc.found && this.rawDocExistsInNamespace(aliasMatchDoc, namespace); + + if (foundExactMatch && foundAliasMatch) { + return { + saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + outcome: 'conflict', + }; + } else if (foundExactMatch) { + return { + saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + outcome: 'exactMatch', + }; + } else if (foundAliasMatch) { + return { + saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc), + outcome: 'aliasMatch', + }; + } + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } /** @@ -1718,7 +1798,7 @@ export class SavedObjectsRepository { if (this._registry.isSingleNamespace(type)) { savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)]; } - return omit(savedObject, 'namespace') as SavedObject; + return omit(savedObject, ['namespace']) as SavedObject; } /** @@ -1814,6 +1894,43 @@ export class SavedObjectsRepository { } return body as SavedObjectsRawDoc; } + + private getSavedObjectFromSource( + type: string, + id: string, + doc: { _seq_no: number; _primary_term: number; _source: SavedObjectsRawDocSource } + ): SavedObject { + const { originId, updated_at: updatedAt } = doc._source; + + let namespaces: string[] = []; + if (!this._registry.isNamespaceAgnostic(type)) { + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; + } + + return { + id, + type, + namespaces, + ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + coreMigrationVersion: doc._source.coreMigrationVersion, + }; + } + + private async resolveExactMatch( + type: string, + id: string, + options: SavedObjectsBaseOptions + ): Promise> { + const object = await this.get(type, id, options); + return { saved_object: object, outcome: 'exactMatch' }; + } } function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 75269d3a77f65..2dd2bcce7a245 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -20,6 +20,7 @@ const create = () => bulkGet: jest.fn(), find: jest.fn(), get: jest.fn(), + resolve: jest.fn(), update: jest.fn(), addToNamespaces: jest.fn(), deleteFromNamespaces: jest.fn(), diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 5cee6bc274f9b..e6409fb853bd8 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -115,6 +115,22 @@ test(`#get`, async () => { expect(result).toBe(returnValue); }); +test(`#resolve`, async () => { + const returnValue = Symbol(); + const mockRepository = { + resolve: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const id = Symbol(); + const options = Symbol(); + const result = await client.resolve(type, id, options); + + expect(mockRepository.resolve).toHaveBeenCalledWith(type, id, options); + expect(result).toBe(returnValue); +}); + test(`#update`, async () => { const returnValue = Symbol(); const mockRepository = { diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index ca1d404e010bd..d17f6b082096f 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -34,6 +34,16 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { version?: string; /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** + * A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current + * Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the + * current Kibana version, it will result in an error. + * + * @remarks + * Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` + * field set and you want to create it again. + */ + coreMigrationVersion?: string; references?: SavedObjectReference[]; /** The Elasticsearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; @@ -60,6 +70,16 @@ export interface SavedObjectsBulkCreateObject { references?: SavedObjectReference[]; /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** + * A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current + * Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the + * current Kibana version, it will result in an error. + * + * @remarks + * Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` + * field set and you want to create it again. + */ + coreMigrationVersion?: string; /** Optional ID of the original saved object, if this object's `id` was regenerated */ originId?: string; /** @@ -273,6 +293,24 @@ export interface SavedObjectsUpdateResponse references: SavedObjectReference[] | undefined; } +/** + * + * @public + */ +export interface SavedObjectsResolveResponse { + saved_object: SavedObject; + /** + * The outcome for a successful `resolve` call is one of the following values: + * + * * `'exactMatch'` -- One document exactly matched the given ID. + * * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different + * than the given ID. + * * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the + * `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID. + */ + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; +} + /** * * @public @@ -379,6 +417,21 @@ export class SavedObjectsClient { return await this._repository.get(type, id, options); } + /** + * Resolves a single object, using any legacy URL alias if it exists + * + * @param type - The type of SavedObject to retrieve + * @param id - The ID of the SavedObject to retrieve + * @param options + */ + async resolve( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> { + return await this._repository.resolve(type, id, options); + } + /** * Updates an SavedObject * diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index cbd8b415d9d31..7fab03aab4d0f 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -241,6 +241,41 @@ export interface SavedObjectsType { * An optional map of {@link SavedObjectMigrationFn | migrations} or a function returning a map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type. */ migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap); + /** + * If defined, objects of this type will be converted to multi-namespace objects when migrating to this version. + * + * Requirements: + * + * 1. This string value must be a valid semver version + * 2. This type must have previously specified {@link SavedObjectsNamespaceType | `namespaceType: 'single'`} + * 3. This type must also specify {@link SavedObjectsNamespaceType | `namespaceType: 'multiple'`} + * + * Example of a single-namespace type in 7.10: + * + * ```ts + * { + * name: 'foo', + * hidden: false, + * namespaceType: 'single', + * mappings: {...} + * } + * ``` + * + * Example after converting to a multi-namespace type in 7.11: + * + * ```ts + * { + * name: 'foo', + * hidden: false, + * namespaceType: 'multiple', + * mappings: {...}, + * convertToMultiNamespaceTypeVersion: '7.11.0' + * } + * ``` + * + * Note: a migration function can be optionally specified for the same version. + */ + convertToMultiNamespaceTypeVersion?: string; /** * An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 3a8d7f4f0b0ff..d0ba6aa1900c7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -680,6 +680,20 @@ export interface CoreUsageStats { // (undocumented) 'apiCalls.savedObjectsImport.total'?: number; // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolve.total'?: number; + // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number; // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; @@ -2033,6 +2047,7 @@ export type SafeRouteMethod = 'get' | 'options'; // @public (undocumented) export interface SavedObject { attributes: T; + coreMigrationVersion?: string; // Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2116,6 +2131,7 @@ export interface SavedObjectsBaseOptions { export interface SavedObjectsBulkCreateObject { // (undocumented) attributes: T; + coreMigrationVersion?: string; // (undocumented) id?: string; initialNamespaces?: string[]; @@ -2206,6 +2222,7 @@ export class SavedObjectsClient { find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; + resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } @@ -2276,6 +2293,7 @@ export interface SavedObjectsCoreFieldMapping { // @public (undocumented) export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { + coreMigrationVersion?: string; id?: string; initialNamespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; @@ -2711,6 +2729,11 @@ export interface SavedObjectsRawDoc { _source: SavedObjectsRawDocSource; } +// @public +export interface SavedObjectsRawDocParseOptions { + namespaceTreatment?: 'strict' | 'lax'; +} + // @public (undocumented) export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions { refresh?: boolean; @@ -2741,6 +2764,7 @@ export class SavedObjectsRepository { get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; + resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } @@ -2758,13 +2782,21 @@ export interface SavedObjectsResolveImportErrorsOptions { retries: SavedObjectsImportRetry[]; } +// @public (undocumented) +export interface SavedObjectsResolveResponse { + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + // (undocumented) + saved_object: SavedObject; +} + // @public export class SavedObjectsSerializer { // @internal constructor(registry: ISavedObjectTypeRegistry); generateRawId(namespace: string | undefined, type: string, id: string): string; - isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean; - rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc; + generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string; + isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; + rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; } @@ -2799,6 +2831,7 @@ export interface SavedObjectStatusMeta { // @public (undocumented) export interface SavedObjectsType { convertToAliasScript?: string; + convertToMultiNamespaceTypeVersion?: string; hidden: boolean; indexPattern?: string; management?: SavedObjectsTypeManagementDefinition; diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 38b8ad0fc5325..c19f1febc97b1 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -82,6 +82,8 @@ export interface SavedObject { references: SavedObjectReference[]; /** {@inheritdoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** A semver value that is used when upgrading objects between Kibana versions. */ + coreMigrationVersion?: string; /** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */ namespaces?: string[]; /** diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 15594dc80c888..84a82511d5a5e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1118,7 +1118,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("src/core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index 1bc4c70e77064..9b83cdd69a545 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -154,6 +154,13 @@ export function getCoreUsageCollector( 'apiCalls.savedObjectsGet.namespace.custom.total': { type: 'long' }, 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes': { type: 'long' }, 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsResolve.total': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no': { type: 'long' }, 'apiCalls.savedObjectsUpdate.total': { type: 'long' }, 'apiCalls.savedObjectsUpdate.namespace.default.total': { type: 'long' }, 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes': { type: 'long' }, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index bed142f165d64..50a08d96de951 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3864,6 +3864,27 @@ "apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no": { "type": "long" }, + "apiCalls.savedObjectsResolve.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no": { + "type": "long" + }, "apiCalls.savedObjectsUpdate.total": { "type": "long" }, diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 903332a0a930f..a548172365b07 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -21,6 +22,7 @@ export default function ({ getService }: FtrProviderContext) { attributes: { title: 'An existing visualization', }, + coreMigrationVersion: '1.2.3', }, { type: 'dashboard', @@ -32,6 +34,12 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('_bulk_create', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -65,6 +73,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: { dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, }, + coreMigrationVersion: KIBANA_VERSION, references: [], namespaces: ['default'], }, @@ -112,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: { visualization: resp.body.saved_objects[0].migrationVersion.visualization, }, + coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version }, { type: 'dashboard', @@ -126,6 +136,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: { dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, }, + coreMigrationVersion: KIBANA_VERSION, }, ], }); diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index e552c08a58cf0..46631225f8e8a 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -30,6 +31,12 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('_bulk_get', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -58,6 +65,7 @@ export default function ({ getService }: FtrProviderContext) { resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, }, migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['default'], references: [ { @@ -87,6 +95,7 @@ export default function ({ getService }: FtrProviderContext) { }, namespaces: ['default'], migrationVersion: resp.body.saved_objects[2].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, references: [], }, ], diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index b1cd5a8dfdae4..551e082630e8f 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -15,6 +16,12 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('create', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -42,6 +49,7 @@ export default function ({ getService }: FtrProviderContext) { id: resp.body.id, type: 'visualization', migrationVersion: resp.body.migrationVersion, + coreMigrationVersion: KIBANA_VERSION, updated_at: resp.body.updated_at, version: resp.body.version, attributes: { @@ -53,6 +61,21 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body.migrationVersion).to.be.ok(); }); }); + + it('result should be updated to the latest coreMigrationVersion', async () => { + await supertest + .post(`/api/saved_objects/visualization`) + .send({ + attributes: { + title: 'My favorite vis', + }, + coreMigrationVersion: '1.2.3', + }) + .expect(200) + .then((resp) => { + expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); + }); + }); }); describe('without kibana index', () => { @@ -86,6 +109,7 @@ export default function ({ getService }: FtrProviderContext) { id: resp.body.id, type: 'visualization', migrationVersion: resp.body.migrationVersion, + coreMigrationVersion: KIBANA_VERSION, updated_at: resp.body.updated_at, version: resp.body.version, attributes: { @@ -99,6 +123,21 @@ export default function ({ getService }: FtrProviderContext) { expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true); }); + + it('result should have the latest coreMigrationVersion', async () => { + await supertest + .post(`/api/saved_objects/visualization`) + .send({ + attributes: { + title: 'My favorite vis', + }, + coreMigrationVersion: '1.2.3', + }) + .expect(200) + .then((resp) => { + expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index a84f3050fdd17..a45191f24d872 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; function ndjsonToObject(input: string) { return input.split('\n').map((str) => JSON.parse(str)); @@ -18,6 +19,12 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('export', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { describe('basic amount of saved objects', () => { before(() => esArchiver.load('saved_objects/basic')); @@ -312,6 +319,7 @@ export default function ({ getService }: FtrProviderContext) { }, id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', migrationVersion: objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', @@ -371,6 +379,7 @@ export default function ({ getService }: FtrProviderContext) { }, id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', migrationVersion: objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', @@ -435,6 +444,7 @@ export default function ({ getService }: FtrProviderContext) { }, id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', migrationVersion: objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index a3ce70888049c..7aa4de86baa69 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { SavedObject } from '../../../../src/core/server'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -16,6 +17,12 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('find', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -39,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) { }, score: 0, migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['default'], references: [ { @@ -134,6 +142,7 @@ export default function ({ getService }: FtrProviderContext) { title: 'Count of requests', }, migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['default'], score: 0, references: [ @@ -170,6 +179,7 @@ export default function ({ getService }: FtrProviderContext) { title: 'Count of requests', }, migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['default'], score: 0, references: [ @@ -187,6 +197,7 @@ export default function ({ getService }: FtrProviderContext) { }, id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['foo-ns'], references: [ { @@ -202,7 +213,6 @@ export default function ({ getService }: FtrProviderContext) { }, ], }); - expect(resp.body.saved_objects[0].migrationVersion).to.be.ok(); })); }); @@ -244,6 +254,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, updated_at: '2017-09-21T18:51:23.794Z', version: 'WzIsMV0=', }, diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index 7134917122177..ff47b9df218dc 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -15,6 +16,12 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('get', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -30,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { updated_at: '2017-09-21T18:51:23.794Z', version: resp.body.version, migrationVersion: resp.body.migrationVersion, + coreMigrationVersion: KIBANA_VERSION, attributes: { title: 'Count of requests', description: '', diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index 0e07b3c1ed060..2f63a4a7cce0a 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -12,15 +12,16 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('saved_objects', () => { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); 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')); - loadTestFile(require.resolve('./bulk_update')); - loadTestFile(require.resolve('./migrations')); }); } diff --git a/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts new file mode 100644 index 0000000000000..e278bd3d50034 --- /dev/null +++ b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts @@ -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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export async function getKibanaVersion(getService: FtrProviderContext['getService']) { + const kibanaServer = getService('kibanaServer'); + const kibanaVersion = await kibanaServer.version.get(); + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + return kibanaVersion; +} diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index 9bb820b2f8414..0b06b675f60c0 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -10,10 +10,11 @@ * 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 { ElasticsearchClient, SavedObjectMigrationMap, SavedObjectsType } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsType } from 'src/core/server'; import { SearchResponse } from '../../../../src/core/server/elasticsearch/client'; import { DocumentMigrator, @@ -28,6 +29,26 @@ import { } 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: {} }, +}; + function getLogMock() { return { debug() {}, @@ -61,16 +82,22 @@ export default ({ getService }: FtrProviderContext) => { bar: { properties: { mynum: { type: 'integer' } } }, }; - const migrations: Record = { - foo: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + const savedObjectTypes: SavedObjectsType[] = [ + { + ...FOO_TYPE, + migrations: { + '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + }, }, - bar: { - '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), + { + ...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 }); await createDocs({ esClient, index, docs: originalDocs }); @@ -107,7 +134,7 @@ export default ({ getService }: FtrProviderContext) => { const result = await migrateIndex({ esClient, index, - migrations, + savedObjectTypes, mappingProperties, obsoleteIndexTemplatePattern: 'migration_a*', }); @@ -129,13 +156,7 @@ export default ({ getService }: FtrProviderContext) => { }); // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql([ - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } }, - { id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } }, - { id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } }, - ]); + 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([ @@ -145,6 +166,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'bar:o', @@ -152,14 +174,22 @@ export default ({ getService }: FtrProviderContext) => { 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: 'baz:u', type: 'baz', baz: { title: 'Terrific!' }, references: [] }, { id: 'foo:a', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'foo:e', @@ -167,6 +197,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, ]); }); @@ -185,28 +216,46 @@ export default ({ getService }: FtrProviderContext) => { bar: { properties: { mynum: { type: 'integer' } } }, }; - const migrations: Record = { - foo: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + let savedObjectTypes: SavedObjectsType[] = [ + { + ...FOO_TYPE, + migrations: { + '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + }, }, - bar: { - '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), + { + ...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 }); await createDocs({ esClient, index, docs: originalDocs }); - await migrateIndex({ esClient, index, migrations, mappingProperties }); + await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); // @ts-expect-error name doesn't exist on mynum type mappingProperties.bar.properties.name = { type: 'keyword' }; - migrations.foo['2.0.1'] = (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`); - migrations.bar['2.3.4'] = (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`); + 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, migrations, mappingProperties }); + await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); // The index for the initial migration has not been destroyed... expect(await fetchDocs(esClient, `${index}_2`)).to.eql([ @@ -216,6 +265,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'bar:o', @@ -223,6 +273,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { bar: '1.9.0' }, bar: { mynum: 6 }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'foo:a', @@ -230,6 +281,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'foo:e', @@ -237,6 +289,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, ]); @@ -248,6 +301,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { bar: '2.3.4' }, bar: { mynum: 68, name: 'NAME i' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'bar:o', @@ -255,6 +309,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { bar: '2.3.4' }, bar: { mynum: 6, name: 'NAME o' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'foo:a', @@ -262,6 +317,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOO Av2' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, { id: 'foo:e', @@ -269,6 +325,7 @@ export default ({ getService }: FtrProviderContext) => { migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOOEYv2' }, references: [], + coreMigrationVersion: KIBANA_VERSION, }, ]); }); @@ -281,18 +338,21 @@ export default ({ getService }: FtrProviderContext) => { foo: { properties: { name: { type: 'text' } } }, }; - const migrations: Record = { - foo: { - '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), + const savedObjectTypes: SavedObjectsType[] = [ + { + ...FOO_TYPE, + migrations: { + '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), + }, }, - }; + ]; await createIndex({ esClient, index }); await createDocs({ esClient, index, docs: originalDocs }); const result = await Promise.all([ - migrateIndex({ esClient, index, migrations, mappingProperties }), - migrateIndex({ esClient, index, migrations, mappingProperties }), + migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), + migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), ]); // The polling instance and the migrating instance should both @@ -327,9 +387,170 @@ export default ({ getService }: FtrProviderContext) => { 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' } } }, + }; + + const savedObjectTypes: SavedObjectsType[] = [ + { + ...FOO_TYPE, + namespaceType: 'multiple', + convertToMultiNamespaceTypeVersion: '1.0.0', + }, + { + ...BAR_TYPE, + namespaceType: 'multiple', + convertToMultiNamespaceTypeVersion: '2.0.0', + }, + BAZ_TYPE, // must be registered for reference transforms to be applied to objects of this type + ]; + + await createIndex({ esClient, index }); + 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': { + 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': { + 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) + ); + }); }); }; @@ -340,6 +561,30 @@ async function createIndex({ esClient, index }: { esClient: ElasticsearchClient; 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', + }, }; await esClient.indices.create({ index, @@ -369,23 +614,23 @@ async function createDocs({ async function migrateIndex({ esClient, index, - migrations, + savedObjectTypes, mappingProperties, obsoleteIndexTemplatePattern, }: { esClient: ElasticsearchClient; index: string; - migrations: Record; + savedObjectTypes: SavedObjectsType[]; mappingProperties: SavedObjectsTypeMappingDefinitions; obsoleteIndexTemplatePattern?: string; }) { const typeRegistry = new SavedObjectTypeRegistry(); - const types = migrationsToTypes(migrations); - types.forEach((type) => typeRegistry.registerType(type)); + savedObjectTypes.forEach((type) => typeRegistry.registerType(type)); const documentMigrator = new DocumentMigrator({ - kibanaVersion: '99.9.9', + 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(), }); @@ -395,6 +640,7 @@ async function migrateIndex({ client: createMigrationEsClient(esClient, getLogMock()), documentMigrator, index, + kibanaVersion: KIBANA_VERSION, obsoleteIndexTemplatePattern, mappingProperties, batchSize: 10, @@ -407,18 +653,6 @@ async function migrateIndex({ return await migrator.migrate(); } -function migrationsToTypes( - migrations: Record -): SavedObjectsType[] { - return Object.entries(migrations).map(([type, migrationsMap]) => ({ - name: type, - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, - migrations: { ...migrationsMap }, - })); -} - async function fetchDocs(esClient: ElasticsearchClient, index: string) { const { body } = await esClient.search>({ index }); @@ -427,5 +661,9 @@ async function fetchDocs(esClient: ElasticsearchClient, index: string) { ...h._source, id: h._id, })) - .sort((a, b) => a.id.localeCompare(b.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); } diff --git a/test/api_integration/apis/saved_objects/resolve.ts b/test/api_integration/apis/saved_objects/resolve.ts new file mode 100644 index 0000000000000..b71d5e3003495 --- /dev/null +++ b/test/api_integration/apis/saved_objects/resolve.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import { getKibanaVersion } from './lib/saved_objects_test_utils'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + const esArchiver = getService('esArchiver'); + + describe('resolve', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await getKibanaVersion(getService); + }); + + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200', async () => + await supertest + .get(`/api/saved_objects/resolve/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + saved_object: { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.saved_object.version, + migrationVersion: resp.body.saved_object.migrationVersion, + coreMigrationVersion: KIBANA_VERSION, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.saved_object.attributes.visState, + uiStateJSON: resp.body.saved_object.attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.saved_object.attributes.kibanaSavedObjectMeta, + }, + references: [ + { + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + }, + ], + namespaces: ['default'], + }, + outcome: 'exactMatch', + }); + expect(resp.body.saved_object.migrationVersion).to.be.ok(); + })); + + describe('doc does not exist', () => { + it('should return same generic error as when index does not exist', async () => + await supertest + .get(`/api/saved_objects/resolve/visualization/foobar`) + .expect(404) + .then((resp) => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: 'Saved object [visualization/foobar] not found', + statusCode: 404, + }); + })); + }); + }); + + describe('without kibana index', () => { + before( + async () => + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + ); + + it('should return basic 404 without mentioning index', async () => + await supertest + .get('/api/saved_objects/resolve/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab') + .expect(404) + .then((resp) => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: + 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 404, + }); + })); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index d7b486e8ab5cf..acc01c73de674 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -14,8 +14,17 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('find', () => { + let KIBANA_VERSION: string; + + before(async () => { + KIBANA_VERSION = await kibanaServer.version.get(); + expect(typeof KIBANA_VERSION).to.eql('string'); + expect(KIBANA_VERSION.length).to.be.greaterThan(0); + }); + describe('with kibana index', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); @@ -38,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) { title: 'Count of requests', }, migrationVersion: resp.body.saved_objects[0].migrationVersion, + coreMigrationVersion: KIBANA_VERSION, namespaces: ['default'], references: [ { diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 85ec08fb7388d..90700f8fa7521 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -1452,6 +1452,140 @@ describe('#get', () => { }); }); +describe('#resolve', () => { + it('redirects request to underlying base client and does not alter response if type is not registered', async () => { + const mockedResponse = { + saved_object: { + id: 'some-id', + type: 'unknown-type', + attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, + references: [], + }, + outcome: 'exactMatch' as 'exactMatch', + }; + + mockBaseClient.resolve.mockResolvedValue(mockedResponse); + + const options = { namespace: 'some-ns' }; + await expect(wrapper.resolve('unknown-type', 'some-id', options)).resolves.toEqual( + mockedResponse + ); + expect(mockBaseClient.resolve).toHaveBeenCalledTimes(1); + expect(mockBaseClient.resolve).toHaveBeenCalledWith('unknown-type', 'some-id', options); + }); + + it('redirects request to underlying base client and strips encrypted attributes except for ones with `dangerouslyExposeValue` set to `true` if type is registered', async () => { + const mockedResponse = { + saved_object: { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + references: [], + }, + outcome: 'exactMatch' as 'exactMatch', + }; + + mockBaseClient.resolve.mockResolvedValue(mockedResponse); + + const options = { namespace: 'some-ns' }; + await expect(wrapper.resolve('known-type', 'some-id', options)).resolves.toEqual({ + ...mockedResponse, + saved_object: { + ...mockedResponse.saved_object, + attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' }, + }, + }); + expect(mockBaseClient.resolve).toHaveBeenCalledTimes(1); + expect(mockBaseClient.resolve).toHaveBeenCalledWith('known-type', 'some-id', options); + + expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes( + 1 + ); + expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'some-id', namespace: 'some-ns' }, + { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + undefined, + { user: mockAuthenticatedUser() } + ); + }); + + it('includes both attributes and error with modified outcome if decryption fails.', async () => { + const mockedResponse = { + saved_object: { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + references: [], + }, + outcome: 'exactMatch' as 'exactMatch', + }; + + mockBaseClient.resolve.mockResolvedValue(mockedResponse); + + const decryptionError = new EncryptionError( + 'something failed', + 'attrNotSoSecret', + EncryptionErrorOperation.Decryption + ); + encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes.mockResolvedValue({ + attributes: { attrOne: 'one', attrThree: 'three' }, + error: decryptionError, + }); + + const options = { namespace: 'some-ns' }; + await expect(wrapper.resolve('known-type', 'some-id', options)).resolves.toEqual({ + ...mockedResponse, + saved_object: { + ...mockedResponse.saved_object, + attributes: { attrOne: 'one', attrThree: 'three' }, + error: decryptionError, + }, + }); + expect(mockBaseClient.resolve).toHaveBeenCalledTimes(1); + expect(mockBaseClient.resolve).toHaveBeenCalledWith('known-type', 'some-id', options); + + expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes( + 1 + ); + expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'some-id', namespace: 'some-ns' }, + { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + undefined, + { user: mockAuthenticatedUser() } + ); + }); + + it('fails if base client fails', async () => { + const failureReason = new Error('Something bad happened...'); + mockBaseClient.resolve.mockRejectedValue(failureReason); + + await expect(wrapper.resolve('known-type', 'some-id')).rejects.toThrowError(failureReason); + + expect(mockBaseClient.resolve).toHaveBeenCalledTimes(1); + expect(mockBaseClient.resolve).toHaveBeenCalledWith('known-type', 'some-id', undefined); + }); +}); + describe('#update', () => { it('redirects request to underlying base client if type is not registered', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 313e7c7da9eba..c3008a8e86505 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -181,6 +181,19 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon ); } + public async resolve(type: string, id: string, options?: SavedObjectsBaseOptions) { + const resolveResult = await this.options.baseClient.resolve(type, id, options); + const object = await this.handleEncryptedAttributesInResponse( + resolveResult.saved_object, + undefined as unknown, + getDescriptorNamespace(this.options.baseTypeRegistry, type, options?.namespace) + ); + return { + ...resolveResult, + saved_object: object, + }; + } + public async update( type: string, id: string, diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index f7e41bce674ee..ef77170de69e2 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -115,6 +115,12 @@ describe('#savedObjectEvent', () => { savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, }) ).not.toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.RESOLVE, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).not.toBeUndefined(); expect( savedObjectEvent({ action: SavedObjectAction.FIND, @@ -136,6 +142,18 @@ describe('#savedObjectEvent', () => { savedObject: { type: 'telemetry', id: 'SAVED_OBJECT_ID' }, }) ).toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.RESOLVE, + savedObject: { type: 'config', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.RESOLVE, + savedObject: { type: 'telemetry', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); expect( savedObjectEvent({ action: SavedObjectAction.FIND, diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index b6538af31bd60..f7d99877bca27 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -182,6 +182,7 @@ export function userLoginEvent({ export enum SavedObjectAction { CREATE = 'saved_object_create', GET = 'saved_object_get', + RESOLVE = 'saved_object_resolve', UPDATE = 'saved_object_update', DELETE = 'saved_object_delete', FIND = 'saved_object_find', @@ -195,6 +196,7 @@ type VerbsTuple = [string, string, string]; const savedObjectAuditVerbs: Record = { saved_object_create: ['create', 'creating', 'created'], saved_object_get: ['access', 'accessing', 'accessed'], + saved_object_resolve: ['resolve', 'resolving', 'resolved'], saved_object_update: ['update', 'updating', 'updated'], saved_object_delete: ['delete', 'deleting', 'deleted'], saved_object_find: ['access', 'accessing', 'accessed'], @@ -210,6 +212,7 @@ const savedObjectAuditVerbs: Record = { const savedObjectAuditTypes: Record = { saved_object_create: EventType.CREATION, saved_object_get: EventType.ACCESS, + saved_object_resolve: EventType.ACCESS, saved_object_update: EventType.CHANGE, saved_object_delete: EventType.DELETION, saved_object_find: EventType.ACCESS, diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 15ca8bac89bd6..5c421776d54f0 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -175,6 +175,7 @@ const expectObjectNamespaceFiltering = async ( // we don't know which base client method will be called; mock them all clientOpts.baseClient.create.mockReturnValue(returnValue as any); clientOpts.baseClient.get.mockReturnValue(returnValue as any); + // 'resolve' is excluded because it has a specific test case written for it clientOpts.baseClient.update.mockReturnValue(returnValue as any); clientOpts.baseClient.addToNamespaces.mockReturnValue(returnValue as any); clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(returnValue as any); @@ -985,6 +986,82 @@ describe('#get', () => { }); }); +describe('#resolve', () => { + const type = 'foo'; + const id = `${type}-id`; + const namespace = 'some-ns'; + const resolvedId = 'another-id'; // success audit records include the resolved ID, not the requested ID + const mockResult = { saved_object: { id: resolvedId } }; // mock result needs to have ID for audit logging + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.resolve, { type, id }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const options = { namespace }; + await expectForbiddenError(client.resolve, { type, id, options }, 'resolve'); + }); + + test(`returns result of baseClient.resolve when authorized`, async () => { + const apiCallReturnValue = mockResult; + clientOpts.baseClient.resolve.mockReturnValue(apiCallReturnValue as any); + + const options = { namespace }; + const result = await expectSuccess(client.resolve, { type, id, options }, 'resolve'); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const options = { namespace }; + await expectPrivilegeCheck(client.resolve, { type, id, options }, namespace); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const options = { namespace }; + + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // privilege check for namespace filtering + ); + + const namespaces = ['some-other-namespace', '*', namespace]; + const returnValue = { saved_object: { namespaces, id: resolvedId, foo: 'bar' } }; + clientOpts.baseClient.resolve.mockReturnValue(returnValue as any); + + const result = await client.resolve(type, id, options); + // we will never redact the "All Spaces" ID + expect(result).toEqual({ + saved_object: expect.objectContaining({ namespaces: ['*', namespace, '?'] }), + }); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith( + 'login:', + ['some-other-namespace'] + // when we check what namespaces to redact, we don't check privileges for '*', only actual space IDs + // we don't check privileges for authorizedNamespace either, as that was already checked earlier in the operation + ); + }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = mockResult; + clientOpts.baseClient.resolve.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.resolve, { type, id, options }, 'resolve'); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_resolve', EventOutcome.SUCCESS, { type, id: resolvedId }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.resolve(type, id, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_resolve', EventOutcome.FAILURE, { type, id }); + }); +}); + describe('#deleteFromNamespaces', () => { const type = 'foo'; const id = `${type}-id`; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 765274a839efa..e53bb742e2179 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -335,6 +335,42 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } + public async resolve( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ) { + try { + const args = { type, id, options }; + await this.ensureAuthorized(type, 'get', options.namespace, { args, auditAction: 'resolve' }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.RESOLVE, + savedObject: { type, id }, + error, + }) + ); + throw error; + } + + const resolveResult = await this.baseClient.resolve(type, id, options); + + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.RESOLVE, + savedObject: { type, id: resolveResult.saved_object.id }, + }) + ); + + return { + ...resolveResult, + saved_object: await this.redactSavedObjectNamespaces(resolveResult.saved_object, [ + options.namespace, + ]), + }; + } + public async update( type: string, id: string, diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 4fd9529507335..a79651c1ae9a6 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -103,6 +103,37 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); + describe('#resolve', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + await expect(client.resolve('foo', '', { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + const expectedReturnValue = { + saved_object: createMockResponse(), + outcome: 'exactMatch' as 'exactMatch', // outcome doesn't matter, just including it for type safety + }; + baseClient.resolve.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const type = Symbol(); + const id = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-expect-error + const actualReturnValue = await client.resolve(type, id, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.resolve).toHaveBeenCalledWith(type, id, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + describe('#bulkGet', () => { test(`throws error if options.namespace is specified`, async () => { const { client } = createSpacesSavedObjectsClient(); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 049bd88085ed5..bd09b8237a468 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -246,6 +246,28 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { }); } + /** + * Resolves a single object, using any legacy URL alias if it exists + * + * @param type - The type of SavedObject to retrieve + * @param id - The ID of the SavedObject to retrieve + * @param {object} [options={}] + * @property {string} [options.namespace] + * @returns {promise} - { saved_object, outcome } + */ + public async resolve( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.resolve(type, id, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + /** * Updates an object * diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index d9d5c6f9c5808..32cae675dea74 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -502,3 +502,119 @@ "type": "doc" } } + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "resolvetype:exact-match", + "source": { + "type": "resolvetype", + "updated_at": "2017-09-21T18:51:23.794Z", + "resolvetype": { + "title": "Resolve outcome exactMatch" + }, + "namespaces": ["default", "space_1", "space_2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "resolvetype:alias-match-newid", + "source": { + "type": "resolvetype", + "updated_at": "2017-09-21T18:51:23.794Z", + "resolvetype": { + "title": "Resolve outcome aliasMatch" + }, + "namespaces": ["default", "space_1", "space_2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "legacy-url-alias:space_1:resolvetype:alias-match", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "targetNamespace": "space_1", + "targetType": "resolvetype", + "targetId": "alias-match-newid" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "legacy-url-alias:space_1:resolvetype:disabled", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "targetNamespace": "space_1", + "targetType": "resolvetype", + "targetId": "alias-match-newid", + "disabled": true + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "resolvetype:conflict", + "source": { + "type": "resolvetype", + "updated_at": "2017-09-21T18:51:23.794Z", + "resolvetype": { + "title": "Resolve outcome conflict (1 of 2)" + }, + "namespaces": ["default", "space_1", "space_2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "resolvetype:conflict-newid", + "source": { + "type": "resolvetype", + "updated_at": "2017-09-21T18:51:23.794Z", + "resolvetype": { + "title": "Resolve outcome conflict (2 of 2)" + }, + "namespaces": ["default", "space_1", "space_2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "legacy-url-alias:space_1:resolvetype:conflict", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "targetNamespace": "space_1", + "targetType": "resolvetype", + "targetId": "conflict-newid" + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 73f0e536b9295..561c2ecc56fa2 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -176,6 +176,28 @@ } } }, + "legacy-url-alias": { + "properties": { + "targetNamespace": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + }, + "targetId": { + "type": "keyword" + }, + "lastResolved": { + "type": "date" + }, + "resolveCounter": { + "type": "integer" + }, + "disabled": { + "type": "boolean" + } + } + }, "namespace": { "type": "keyword" }, @@ -185,6 +207,13 @@ "originId": { "type": "keyword" }, + "resolvetype": { + "properties": { + "title": { + "type": "text" + } + } + }, "search": { "properties": { "columns": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts index 45880635586a7..d311e539b1687 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts +++ b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts @@ -64,6 +64,13 @@ export class Plugin { namespaceType: 'single', mappings, }); + core.savedObjects.registerType({ + name: 'resolvetype', + hidden: false, + namespaceType: 'multiple', + management, + mappings, + }); } public start() { diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts new file mode 100644 index 0000000000000..250a3b19710a9 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { SuperTest } from 'supertest'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +export interface ResolveTestDefinition extends TestDefinition { + request: { type: string; id: string }; +} +export type ResolveTestSuite = TestSuite; +export interface ResolveTestCase extends TestCase { + expectedOutcome?: 'exactMatch' | 'aliasMatch' | 'conflict'; + expectedId?: string; +} + +const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + +export const TEST_CASES = Object.freeze({ + EXACT_MATCH: Object.freeze({ + type: 'resolvetype', + id: 'exact-match', + expectedNamespaces: EACH_SPACE, + expectedOutcome: 'exactMatch' as 'exactMatch', + expectedId: 'exact-match', + }), + ALIAS_MATCH: Object.freeze({ + type: 'resolvetype', + id: 'alias-match', + expectedNamespaces: EACH_SPACE, + expectedOutcome: 'aliasMatch' as 'aliasMatch', + expectedId: 'alias-match-newid', + }), + CONFLICT: Object.freeze({ + type: 'resolvetype', + id: 'conflict', + expectedNamespaces: EACH_SPACE, + expectedOutcome: 'conflict' as 'conflict', // only in space 1, where the alias exists + expectedId: 'conflict', + }), + DISABLED: Object.freeze({ + type: 'resolvetype', + id: 'disabled', + }), + DOES_NOT_EXIST: Object.freeze({ + type: 'resolvetype', + id: 'does-not-exist', + }), + HIDDEN: CASES.HIDDEN, +}); + +export function resolveTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectSavedObjectForbidden = expectResponses.forbiddenTypes('get'); + const expectResponseBody = (testCase: ResolveTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectSavedObjectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body.saved_object || response.body; // errors do not have a saved_object field + const { expectedId: id, expectedOutcome } = testCase; + await expectResponses.permitted(object, { ...testCase, ...(id && { id }) }); + if (expectedOutcome && !testCase.failure) { + expect(response.body.outcome).to.eql(expectedOutcome); + } + } + }; + const createTestDefinitions = ( + testCases: ResolveTestCase | ResolveTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): ResolveTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map((x) => ({ ...x, failure: 403 })); + } + return cases.map((x) => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); + }; + + const makeResolveTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: ResolveTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/resolve/${type}/${id}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeResolveTest(describe); + // @ts-ignore + addTests.only = makeResolveTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} 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 3cc6b85cb97c0..5e9e499ffea18 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 @@ -28,6 +28,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve.ts new file mode 100644 index 0000000000000..94df364c9017c --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + resolveTestSuiteFactory, + TEST_CASES as CASES, + ResolveTestDefinition, +} from '../../common/suites/resolve'; + +const { + SPACE_1: { spaceId: SPACE_1_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.EXACT_MATCH, + { ...CASES.ALIAS_MATCH, ...fail404(spaceId !== SPACE_1_ID) }, + { + ...CASES.CONFLICT, + ...(spaceId !== SPACE_1_ID && { expectedOutcome: 'exactMatch' as 'exactMatch' }), + }, + { ...CASES.DISABLED, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = resolveTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; + + describe('_resolve', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ResolveTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach((user) => { + _addTests(user, unauthorized); + }); + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach((user) => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index c52ba3f595711..46b0992480764 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -28,6 +28,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts new file mode 100644 index 0000000000000..9f37f97881071 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + resolveTestSuiteFactory, + TEST_CASES as CASES, + ResolveTestDefinition, +} from '../../common/suites/resolve'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.EXACT_MATCH }, + { ...CASES.ALIAS_MATCH, ...fail404() }, + { ...CASES.CONFLICT, expectedOutcome: 'exactMatch' as 'exactMatch' }, + { ...CASES.DISABLED, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = resolveTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; + + describe('_resolve', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: ResolveTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach((user) => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index c8050733fc6e9..137596bc20c4c 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -20,6 +20,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve.ts new file mode 100644 index 0000000000000..a6f76fc80044d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { resolveTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/resolve'; + +const { + SPACE_1: { spaceId: SPACE_1_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + CASES.EXACT_MATCH, + { ...CASES.ALIAS_MATCH, ...fail404(spaceId !== SPACE_1_ID) }, + { + ...CASES.CONFLICT, + ...(spaceId !== SPACE_1_ID && { expectedOutcome: 'exactMatch' as 'exactMatch' }), + }, + { ...CASES.DISABLED, ...fail404() }, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = resolveTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId }); + }; + + describe('_resolve', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); + }); + }); +} From 3b728b73cf5720b48e9759c7bb8b845e18ba5b57 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 20 Jan 2021 19:29:04 -0500 Subject: [PATCH 25/28] [Fleet] Use fleet server indices for enrollment keys and to list agents with a feature flag (#86179) --- .../plugins/fleet/common/constants/agent.ts | 2 + .../fleet/common/constants/agent_policy.ts | 2 +- .../common/constants/enrollment_api_key.ts | 2 + .../fleet/common/services/agent_status.ts | 4 +- x-pack/plugins/fleet/common/types/index.ts | 1 + .../fleet/common/types/models/agent_policy.ts | 34 +++ .../common/types/models/enrollment_api_key.ts | 28 +++ .../fleet/mock/plugin_configuration.ts | 1 + .../server/collectors/agent_collectors.ts | 12 +- .../fleet/server/collectors/helpers.ts | 9 +- .../fleet/server/collectors/register.ts | 6 +- .../plugins/fleet/server/constants/index.ts | 3 + x-pack/plugins/fleet/server/index.ts | 1 + x-pack/plugins/fleet/server/mocks.ts | 7 +- x-pack/plugins/fleet/server/plugin.ts | 11 + .../server/routes/agent/acks_handlers.test.ts | 10 +- .../server/routes/agent/acks_handlers.ts | 8 +- .../routes/agent/actions_handlers.test.ts | 14 +- .../server/routes/agent/actions_handlers.ts | 3 +- .../fleet/server/routes/agent/handlers.ts | 34 ++- .../fleet/server/routes/agent/index.ts | 6 + .../server/routes/agent/unenroll_handler.ts | 8 +- .../server/routes/agent/upgrade_handler.ts | 5 +- .../server/routes/agent_policy/handlers.ts | 14 +- .../routes/enrollment_api_key/handler.ts | 29 ++- .../routes/package_policy/handlers.test.ts | 4 +- .../server/routes/package_policy/handlers.ts | 17 +- .../fleet/server/routes/settings/index.ts | 4 +- .../fleet/server/routes/setup/handlers.ts | 8 +- .../server/services/agent_policy.test.ts | 10 +- .../fleet/server/services/agent_policy.ts | 117 ++++++++-- .../server/services/agent_policy_update.ts | 7 +- .../fleet/server/services/agents/acks.test.ts | 16 +- .../fleet/server/services/agents/acks.ts | 7 +- .../fleet/server/services/agents/actions.ts | 8 +- .../server/services/agents/checkin/index.ts | 9 +- .../server/services/agents/checkin/state.ts | 5 +- .../agents/checkin/state_new_actions.ts | 5 +- .../fleet/server/services/agents/crud.ts | 165 ++++---------- .../services/agents/crud_fleet_server.ts | 197 +++++++++++++++++ .../fleet/server/services/agents/crud_so.ts | 195 +++++++++++++++++ .../fleet/server/services/agents/helpers.ts | 21 ++ .../fleet/server/services/agents/reassign.ts | 6 +- .../server/services/agents/status.test.ts | 14 +- .../fleet/server/services/agents/status.ts | 8 +- .../fleet/server/services/agents/unenroll.ts | 16 +- .../fleet/server/services/agents/update.ts | 5 +- .../fleet/server/services/agents/upgrade.ts | 5 +- .../services/api_keys/enrollment_api_key.ts | 166 +++----------- .../enrollment_api_key_fleet_server.ts | 205 ++++++++++++++++++ .../api_keys/enrollment_api_key_so.ts | 174 +++++++++++++++ .../fleet/server/services/app_context.ts | 20 +- .../server/services/fleet_server_migration.ts | 75 +++++++ x-pack/plugins/fleet/server/services/index.ts | 8 +- .../server/services/package_policy.test.ts | 4 +- .../fleet/server/services/package_policy.ts | 29 ++- .../fleet/server/services/setup.test.ts | 6 +- x-pack/plugins/fleet/server/services/setup.ts | 15 +- x-pack/plugins/fleet/server/types/index.tsx | 2 + .../endpoint/lib/policy/license_watch.test.ts | 15 +- .../endpoint/lib/policy/license_watch.ts | 14 +- .../endpoint/routes/metadata/handlers.ts | 7 +- .../metadata/support/agent_status.test.ts | 37 +++- .../routes/metadata/support/agent_status.ts | 5 +- .../routes/metadata/support/unenroll.test.ts | 15 +- .../routes/metadata/support/unenroll.ts | 9 +- .../server/endpoint/routes/policy/handlers.ts | 1 + .../server/endpoint/routes/policy/service.ts | 13 +- .../manifest_manager/manifest_manager.test.ts | 2 +- .../manifest_manager/manifest_manager.ts | 8 +- .../security_solution/server/plugin.ts | 1 + 71 files changed, 1528 insertions(+), 406 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts create mode 100644 x-pack/plugins/fleet/server/services/agents/crud_so.ts create mode 100644 x-pack/plugins/fleet/server/services/agents/helpers.ts create mode 100644 x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts create mode 100644 x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts create mode 100644 x-pack/plugins/fleet/server/services/fleet_server_migration.ts diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index 30b8a6b740609..8bfb32b5ed2b0 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -22,3 +22,5 @@ export const AGENT_UPDATE_ACTIONS_INTERVAL_MS = 5000; export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS = 1000; export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL = 5; + +export const AGENTS_INDEX = '.fleet-agents'; diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 5445fbcacf2ec..2dd21fb41b663 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -6,7 +6,7 @@ import { defaultPackages } from './epm'; import { AgentPolicy } from '../types'; export const AGENT_POLICY_SAVED_OBJECT_TYPE = 'ingest-agent-policies'; - +export const AGENT_POLICY_INDEX = '.fleet-policies'; export const agentPolicyStatuses = { Active: 'active', Inactive: 'inactive', diff --git a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts index fd28b6632b15c..ce774f2212461 100644 --- a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts @@ -5,3 +5,5 @@ */ export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys'; + +export const ENROLLMENT_API_KEYS_INDEX = '.fleet-enrollment-api-keys'; diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index 4cf35398bab24..c99cfd11b763f 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -41,7 +41,7 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta } export function buildKueryForEnrollingAgents() { - return `not ${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*`; + return `not (${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*)`; } export function buildKueryForUnenrollingAgents() { @@ -53,7 +53,7 @@ export function buildKueryForOnlineAgents() { } export function buildKueryForErrorAgents() { - return `( ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded )`; + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded`; } export function buildKueryForOfflineAgents() { diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index e0827ef7cf40f..b023052b1328d 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -11,6 +11,7 @@ export interface FleetConfigType { registryUrl?: string; registryProxyUrl?: string; agents: { + fleetServerEnabled: boolean; enabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 75bb2998f2d92..2e29fe148b35f 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -80,3 +80,37 @@ export interface FullAgentPolicyKibanaConfig { protocol: string; path?: string; } + +// Generated from Fleet Server schema.json + +/** + * A policy that an Elastic Agent is attached to + */ +export interface FleetServerPolicy { + /** + * Date/time the policy revision was created + */ + '@timestamp'?: string; + /** + * The ID of the policy + */ + policy_id: string; + /** + * The revision index of the policy + */ + revision_idx: number; + /** + * The coordinator index of the policy + */ + coordinator_idx: number; + /** + * The opaque payload. + */ + data: { + [k: string]: unknown; + }; + /** + * True when this policy is the default policy to start Fleet Server + */ + default_fleet_server: boolean; +} diff --git a/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts b/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts index f39076ce1027b..81dc6889f9946 100644 --- a/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts @@ -15,3 +15,31 @@ export interface EnrollmentAPIKey { } export type EnrollmentAPIKeySOAttributes = Omit; + +// Generated + +/** + * An Elastic Agent enrollment API key + */ +export interface FleetServerEnrollmentAPIKey { + /** + * True when the key is active + */ + active?: boolean; + /** + * The unique identifier for the enrollment key, currently xid + */ + api_key_id: string; + /** + * Api key + */ + api_key: string; + /** + * Enrollment key name + */ + name?: string; + policy_id?: string; + expire_at?: string; + created_at?: string; + updated_at?: string; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 735c1d11a9837..62896289af514 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -13,6 +13,7 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, + fleetServerEnabled: false, tlsCheckDisabled: true, pollingRequestTimeout: 1000, maxConcurrentConnections: 100, diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index fe5e5fa735b24..8925f3386dfb8 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClient } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server'; import * as AgentService from '../services/agents'; export interface AgentUsage { total: number; @@ -13,9 +13,12 @@ export interface AgentUsage { offline: number; } -export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise => { +export const getAgentUsage = async ( + soClient?: SavedObjectsClient, + esClient?: ElasticsearchClient +): Promise => { // TODO: unsure if this case is possible at all. - if (!soClient) { + if (!soClient || !esClient) { return { total: 0, online: 0, @@ -24,7 +27,8 @@ export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise { return core.getStartServices().then(async ([coreStart]) => { const savedObjectsRepo = coreStart.savedObjects.createInternalRepository(); - return new SavedObjectsClient(savedObjectsRepo); + const esClient = coreStart.elasticsearch.client.asInternalUser; + return [new SavedObjectsClient(savedObjectsRepo), esClient]; }); } diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index 35517e6a7a700..7ec04ca6fee41 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -8,7 +8,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup } from 'kibana/server'; import { getIsAgentsEnabled } from './config_collectors'; import { AgentUsage, getAgentUsage } from './agent_collectors'; -import { getInternalSavedObjectsClient } from './helpers'; +import { getInternalClients } from './helpers'; import { PackageUsage, getPackageUsage } from './package_collectors'; import { FleetConfigType } from '..'; @@ -34,10 +34,10 @@ export function registerFleetUsageCollector( type: 'fleet', isReady: () => true, fetch: async () => { - const soClient = await getInternalSavedObjectsClient(core); + const [soClient, esClient] = await getInternalClients(core); return { agents_enabled: getIsAgentsEnabled(config), - agents: await getAgentUsage(soClient), + agents: await getAgentUsage(soClient, esClient), packages: await getPackageUsage(soClient), }; }, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index dbf2fbc362a45..37f8ab041e5a3 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -47,4 +47,7 @@ export { // Defaults DEFAULT_AGENT_POLICY, DEFAULT_OUTPUT, + // Fleet Server index + ENROLLMENT_API_KEYS_INDEX, + AGENTS_INDEX, } from '../../common'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 1fe7013944fd7..672911ccf6fe0 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -37,6 +37,7 @@ export const config: PluginConfigDescriptor = { registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), + fleetServerEnabled: schema.boolean({ defaultValue: false }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), pollingRequestTimeout: schema.number({ defaultValue: AGENT_POLLING_REQUEST_TIMEOUT_MS, diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index 4a897d80acd6d..bf659294f514c 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsServiceMock, +} from 'src/core/server/mocks'; import { FleetAppContext } from './plugin'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../security/server/mocks'; @@ -13,6 +17,7 @@ import { AgentPolicyServiceInterface, AgentService } from './services'; export const createAppContextStartContractMock = (): FleetAppContext => { return { + elasticsearch: elasticsearchServiceMock.createStart(), encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), security: securityMock.createStart(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 8ce17a00acf33..253b614dc228a 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -8,6 +8,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, + ElasticsearchServiceStart, Logger, Plugin, PluginInitializerContext, @@ -80,6 +81,7 @@ import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; import { makeRouterEnforcingSuperuser } from './routes/security'; +import { runFleetServerMigration } from './services/fleet_server_migration'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -96,6 +98,7 @@ export interface FleetStartDeps { } export interface FleetAppContext { + elasticsearch: ElasticsearchServiceStart; encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; @@ -276,6 +279,7 @@ export class FleetPlugin public async start(core: CoreStart, plugins: FleetStartDeps): Promise { await appContextService.start({ + elasticsearch: core.elasticsearch, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, @@ -291,6 +295,13 @@ export class FleetPlugin licenseService.start(this.licensing$); agentCheckinState.start(); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + if (fleetServerEnabled) { + // We need licence to be initialized before using the SO service. + await this.licensing$.pipe(first()).toPromise(); + await runFleetServerMigration(); + } + return { esIndexPatternService: new ESIndexPatternSavedObjectService(), packageService: { diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts index 3d7f5c4a17adb..d775979527afb 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts @@ -6,11 +6,16 @@ import { postAgentAcksHandlerBuilder } from './acks_handlers'; import { + ElasticsearchClient, KibanaResponseFactory, RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { httpServerMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + httpServerMock, + savedObjectsClientMock, +} from '../../../../../../src/core/server/mocks'; import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; import { AckEventSchema } from '../../types/models'; import { AcksService } from '../../services/agents'; @@ -45,9 +50,11 @@ describe('test acks schema', () => { describe('test acks handlers', () => { let mockResponse: jest.Mocked; let mockSavedObjectsClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; beforeEach(() => { mockSavedObjectsClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockResponse = httpServerMock.createResponseFactory(); }); @@ -81,6 +88,7 @@ describe('test acks handlers', () => { id: 'agent', }), getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), + getElasticsearchClientContract: jest.fn().mockReturnValueOnce(mockElasticsearchClient), saveAgentEvents: jest.fn(), } as jest.Mocked; diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index fb320b01dea97..28cd7e57d6537 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -18,6 +18,7 @@ export const postAgentAcksHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); + const esClient = ackService.getElasticsearchClientContract(); const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); const agentEvents = request.body.events as AgentEvent[]; @@ -33,7 +34,12 @@ export const postAgentAcksHandlerBuilder = function ( }); } - const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents); + const agentActions = await ackService.acknowledgeAgentActions( + soClient, + esClient, + agent, + agentEvents + ); if (agentActions.length > 0) { await ackService.saveAgentEvents(soClient, agentEvents); diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 2f08846642985..2674e8c5cedd0 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -6,11 +6,16 @@ import { NewAgentActionSchema } from '../../types/models'; import { + ElasticsearchClient, KibanaResponseFactory, RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + httpServerMock, +} from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; @@ -41,9 +46,11 @@ describe('test actions handlers schema', () => { describe('test actions handlers', () => { let mockResponse: jest.Mocked; let mockSavedObjectsClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; beforeEach(() => { mockSavedObjectsClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockResponse = httpServerMock.createResponseFactory(); }); @@ -84,6 +91,11 @@ describe('test actions handlers', () => { savedObjects: { client: mockSavedObjectsClient, }, + elasticsearch: { + client: { + asInternalUser: mockElasticsearchClient, + }, + }, }, } as unknown) as RequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 64a7795cc9dac..04b92296439c5 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -23,8 +23,9 @@ export const postNewAgentActionHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; - const agent = await actionsService.getAgent(soClient, request.params.agentId); + const agent = await actionsService.getAgent(soClient, esClient, request.params.agentId); const newAgentAction = request.body.action; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index a867196f9762f..0cd53a2313d2a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -39,8 +39,10 @@ export const getAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const agent = await AgentService.getAgent(soClient, request.params.agentId); + const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); const body: GetOneAgentResponse = { item: { @@ -98,8 +100,10 @@ export const deleteAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - await AgentService.deleteAgent(soClient, request.params.agentId); + await AgentService.deleteAgent(soClient, esClient, request.params.agentId); const body = { action: 'deleted', @@ -124,11 +128,13 @@ export const updateAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { await AgentService.updateAgent(soClient, request.params.agentId, { userProvidedMetatada: request.body.user_provided_metadata, }); - const agent = await AgentService.getAgent(soClient, request.params.agentId); + const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); const body = { item: { @@ -156,6 +162,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = appContextService.getInternalUserESClient(); const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { @@ -164,6 +171,7 @@ export const postAgentCheckinHandler: RequestHandler< const signal = abortController.signal; const { actions } = await AgentService.agentCheckin( soClient, + esClient, agent, { events: request.body.events || [], @@ -234,8 +242,10 @@ export const getAgentsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const { agents, total, page, perPage } = await AgentService.listAgents(soClient, { + const { agents, total, page, perPage } = await AgentService.listAgents(soClient, esClient, { page: request.query.page, perPage: request.query.perPage, showInactive: request.query.showInactive, @@ -243,7 +253,7 @@ export const getAgentsHandler: RequestHandler< kuery: request.query.kuery, }); const totalInactive = request.query.showInactive - ? await AgentService.countInactiveAgents(soClient, { + ? await AgentService.countInactiveAgents(soClient, esClient, { kuery: request.query.kuery, }) : 0; @@ -270,8 +280,14 @@ export const putAgentsReassignHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { - await AgentService.reassignAgent(soClient, request.params.agentId, request.body.policy_id); + await AgentService.reassignAgent( + soClient, + esClient, + request.params.agentId, + request.body.policy_id + ); const body: PutAgentReassignResponse = {}; return response.ok({ body }); @@ -293,16 +309,19 @@ export const postBulkAgentsReassignHandler: RequestHandler< } const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { // Reassign by array of IDs const result = Array.isArray(request.body.agents) ? await AgentService.reassignAgents( soClient, + esClient, { agentIds: request.body.agents }, request.body.policy_id ) : await AgentService.reassignAgents( soClient, + esClient, { kuery: request.body.agents }, request.body.policy_id ); @@ -326,10 +345,13 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( soClient, + esClient, request.query.policyId, request.query.kuery ); diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 54a30fbc9320f..c088349a995af 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -294,6 +294,9 @@ export const registerElasticAgentRoutes = (router: IRouter, config: FleetConfigT getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), + getElasticsearchClientContract: appContextService.getInternalUserESClient.bind( + appContextService + ), saveAgentEvents: AgentService.saveAgentEvents, }) ); @@ -313,6 +316,9 @@ export const registerElasticAgentRoutes = (router: IRouter, config: FleetConfigT getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), + getElasticsearchClientContract: appContextService.getInternalUserESClient.bind( + appContextService + ), saveAgentEvents: AgentService.saveAgentEvents, }) ); diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index 861d7c45c6f0a..41c183789f9fd 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -18,9 +18,10 @@ export const postAgentUnenrollHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { if (request.body?.force === true) { - await AgentService.forceUnenrollAgent(soClient, request.params.agentId); + await AgentService.forceUnenrollAgent(soClient, esClient, request.params.agentId); } else { await AgentService.unenrollAgent(soClient, request.params.agentId); } @@ -44,14 +45,15 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< }); } const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const unenrollAgents = request.body?.force === true ? AgentService.forceUnenrollAgents : AgentService.unenrollAgents; try { if (Array.isArray(request.body.agents)) { - await unenrollAgents(soClient, { agentIds: request.body.agents }); + await unenrollAgents(soClient, esClient, { agentIds: request.body.agents }); } else { - await unenrollAgents(soClient, { kuery: request.body.agents }); + await unenrollAgents(soClient, esClient, { kuery: request.body.agents }); } const body: PostBulkAgentUnenrollResponse = {}; diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 93e6609167a2e..7b068674d3829 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -83,6 +83,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, agents, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -98,14 +99,14 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< try { if (Array.isArray(agents)) { - await AgentService.sendUpgradeAgentsActions(soClient, { + await AgentService.sendUpgradeAgentsActions(soClient, esClient, { agentIds: agents, sourceUri, version, force, }); } else { - await AgentService.sendUpgradeAgentsActions(soClient, { + await AgentService.sendUpgradeAgentsActions(soClient, esClient, { kuery: agents, sourceUri, version, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 25aaf5f9a4656..8f7fd4427f586 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -39,6 +39,7 @@ export const getAgentPoliciesHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const { full: withPackagePolicies = false, ...restOfQuery } = request.query; try { const { items, total, page, perPage } = await agentPolicyService.list(soClient, { @@ -55,7 +56,7 @@ export const getAgentPoliciesHandler: RequestHandler< await bluebird.map( items, (agentPolicy: GetAgentPoliciesResponseItem) => - listAgents(soClient, { + listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, @@ -100,6 +101,7 @@ export const createAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; @@ -109,7 +111,7 @@ export const createAgentPolicyHandler: RequestHandler< AgentPolicy, NewPackagePolicy | undefined >([ - agentPolicyService.create(soClient, request.body, { + agentPolicyService.create(soClient, esClient, request.body, { user, }), // If needed, retrieve System package information and build a new package policy for the system package @@ -126,7 +128,7 @@ export const createAgentPolicyHandler: RequestHandler< if (withSysMonitoring && newSysPackagePolicy !== undefined && agentPolicy !== undefined) { newSysPackagePolicy.policy_id = agentPolicy.id; newSysPackagePolicy.namespace = agentPolicy.namespace; - await packagePolicyService.create(soClient, callCluster, newSysPackagePolicy, { + await packagePolicyService.create(soClient, esClient, callCluster, newSysPackagePolicy, { user, bumpRevision: false, }); @@ -152,10 +154,12 @@ export const updateAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const agentPolicy = await agentPolicyService.update( soClient, + esClient, request.params.agentPolicyId, request.body, { @@ -177,10 +181,12 @@ export const copyAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const agentPolicy = await agentPolicyService.copy( soClient, + esClient, request.params.agentPolicyId, request.body, { @@ -203,9 +209,11 @@ export const deleteAgentPoliciesHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { const body: DeleteAgentPolicyResponse = await agentPolicyService.delete( soClient, + esClient, request.body.agentPolicyId ); return response.ok({ diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index afecd7bd7d828..4f54b4e155ea3 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -26,12 +26,18 @@ export const getEnrollmentApiKeysHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(soClient, { - page: request.query.page, - perPage: request.query.perPage, - kuery: request.query.kuery, - }); + const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys( + soClient, + esClient, + { + page: request.query.page, + perPage: request.query.perPage, + kuery: request.query.kuery, + } + ); const body: GetEnrollmentAPIKeysResponse = { list: items, total, page, perPage }; return response.ok({ body }); @@ -45,8 +51,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, { + const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, esClient, { name: request.body.name, expiration: request.body.expiration, agentPolicyId: request.body.policy_id, @@ -64,8 +71,9 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await APIKeyService.deleteEnrollmentApiKey(soClient, request.params.keyId); + await APIKeyService.deleteEnrollmentApiKey(soClient, esClient, request.params.keyId); const body: DeleteEnrollmentAPIKeyResponse = { action: 'deleted' }; @@ -84,8 +92,13 @@ export const getOneEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const apiKey = await APIKeyService.getEnrollmentAPIKey(soClient, request.params.keyId); + const apiKey = await APIKeyService.getEnrollmentAPIKey( + soClient, + esClient, + request.params.keyId + ); const body: GetOneEnrollmentAPIKeyResponse = { item: apiKey }; return response.ok({ body }); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index f9fd6047baa77..90a06563efce5 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -25,7 +25,7 @@ jest.mock('../../services/package_policy', (): { compilePackagePolicyInputs: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), buildPackagePolicyFromPackage: jest.fn(), bulkCreate: jest.fn(), - create: jest.fn((soClient, callCluster, newData) => + create: jest.fn((soClient, esClient, callCluster, newData) => Promise.resolve({ ...newData, inputs: newData.inputs.map((input) => ({ @@ -201,7 +201,7 @@ describe('When calling package policy', () => { ); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packagePolicyServiceMock.create.mock.calls[0][2]).toEqual({ + expect(packagePolicyServiceMock.create.mock.calls[0][3]).toEqual({ policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index be14970de3e0f..bef33c1c98b62 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -74,6 +74,7 @@ export const createPackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; let newData = { ...request.body }; @@ -86,9 +87,15 @@ export const createPackagePolicyHandler: RequestHandler< ); // Create package policy - const packagePolicy = await packagePolicyService.create(soClient, callCluster, newData, { - user, - }); + const packagePolicy = await packagePolicyService.create( + soClient, + esClient, + callCluster, + newData, + { + user, + } + ); const body: CreatePackagePolicyResponse = { item: packagePolicy }; return response.ok({ body, @@ -110,6 +117,7 @@ export const updatePackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const packagePolicy = await packagePolicyService.get(soClient, request.params.packagePolicyId); @@ -131,6 +139,7 @@ export const updatePackagePolicyHandler: RequestHandler< const updatedPackagePolicy = await packagePolicyService.update( soClient, + esClient, request.params.packagePolicyId, { ...newData, package: pkg, inputs }, { user } @@ -149,10 +158,12 @@ export const deletePackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { const body: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, + esClient, request.body.packagePolicyIds, { user } ); diff --git a/x-pack/plugins/fleet/server/routes/settings/index.ts b/x-pack/plugins/fleet/server/routes/settings/index.ts index 4eeff629dc227..6f63043c12a27 100644 --- a/x-pack/plugins/fleet/server/routes/settings/index.ts +++ b/x-pack/plugins/fleet/server/routes/settings/index.ts @@ -36,10 +36,12 @@ export const putSettingsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); + try { const settings = await settingsService.saveSettings(soClient, request.body); - await agentPolicyService.bumpAllAgentPolicies(soClient, { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient, { user: user || undefined, }); const body = { diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index cafccd1895d11..cf0967045f151 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -59,9 +59,10 @@ export const createFleetSetupHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - await setupIngestManager(soClient, callCluster); - await setupFleet(soClient, callCluster, { + await setupIngestManager(soClient, esClient, callCluster); + await setupFleet(soClient, esClient, callCluster, { forceRecreate: request.body?.forceRecreate ?? false, }); @@ -75,11 +76,12 @@ export const createFleetSetupHandler: RequestHandler< export const FleetSetupHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; try { const body: PostIngestSetupResponse = { isInitialized: true }; - await setupIngestManager(soClient, callCluster); + await setupIngestManager(soClient, esClient, callCluster); return response.ok({ body, }); 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 f9a8b63bb83ad..de3647bec0164 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { Output } from '../types'; @@ -78,7 +78,9 @@ describe('agent policy', () => { revision: 1, monitoring_enabled: ['metrics'], }); - await agentPolicyService.bumpRevision(soClient, 'agent-policy'); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + await agentPolicyService.bumpRevision(soClient, esClient, 'agent-policy'); expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); }); @@ -90,7 +92,9 @@ describe('agent policy', () => { revision: 1, monitoring_enabled: ['metrics'], }); - await agentPolicyService.bumpAllAgentPolicies(soClient); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient, undefined); expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 0fd41d074effa..81d98823b5268 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,12 @@ */ import { uniq } from 'lodash'; import { safeLoad } from 'js-yaml'; -import { SavedObjectsClientContract, SavedObjectsBulkUpdateResponse } from 'src/core/server'; +import uuid from 'uuid/v4'; +import { + ElasticsearchClient, + SavedObjectsClientContract, + SavedObjectsBulkUpdateResponse, +} from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DEFAULT_AGENT_POLICY, @@ -26,6 +31,8 @@ import { agentPolicyStatuses, storedPackagePoliciesToAgentInputs, dataTypes, + FleetServerPolicy, + AGENT_POLICY_INDEX, } from '../../common'; import { AgentPolicyNameExistsError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; @@ -36,20 +43,23 @@ import { getSettings } from './settings'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config'; import { isAgentsSetup } from './agents/setup'; +import { appContextService } from './app_context'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; class AgentPolicyService { private triggerAgentPolicyUpdatedEvent = async ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, action: 'created' | 'updated' | 'deleted', agentPolicyId: string ) => { - return agentPolicyUpdateEventHandler(soClient, action, agentPolicyId); + return agentPolicyUpdateEventHandler(soClient, esClient, action, agentPolicyId); }; private async _update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, agentPolicy: Partial, user?: AuthenticatedUser, @@ -78,14 +88,15 @@ class AgentPolicyService { }); if (options.bumpRevision) { - await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', id); } return (await this.get(soClient, id)) as AgentPolicy; } public async ensureDefaultAgentPolicy( - soClient: SavedObjectsClientContract + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient ): Promise<{ created: boolean; defaultAgentPolicy: AgentPolicy; @@ -103,7 +114,7 @@ class AgentPolicyService { return { created: true, - defaultAgentPolicy: await this.create(soClient, newDefaultAgentPolicy), + defaultAgentPolicy: await this.create(soClient, esClient, newDefaultAgentPolicy), }; } @@ -118,6 +129,7 @@ class AgentPolicyService { public async create( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicy: NewAgentPolicy, options?: { id?: string; user?: AuthenticatedUser } ): Promise { @@ -134,7 +146,7 @@ class AgentPolicyService { ); if (!agentPolicy.is_default) { - await this.triggerAgentPolicyUpdatedEvent(soClient, 'created', newSo.id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id); } return { id: newSo.id, ...newSo.attributes }; @@ -244,6 +256,7 @@ class AgentPolicyService { public async update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, agentPolicy: Partial, options?: { user?: AuthenticatedUser } @@ -254,11 +267,12 @@ class AgentPolicyService { name: agentPolicy.name, }); } - return this._update(soClient, id, agentPolicy, options?.user); + return this._update(soClient, esClient, id, agentPolicy, options?.user); } public async copy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, newAgentPolicyProps: Pick, options?: { user?: AuthenticatedUser } @@ -272,6 +286,7 @@ class AgentPolicyService { const { namespace, monitoring_enabled } = baseAgentPolicy; const newAgentPolicy = await this.create( soClient, + esClient, { namespace, monitoring_enabled, @@ -288,10 +303,16 @@ class AgentPolicyService { return newPackagePolicy; } ); - await packagePolicyService.bulkCreate(soClient, newPackagePolicies, newAgentPolicy.id, { - ...options, - bumpRevision: false, - }); + await packagePolicyService.bulkCreate( + soClient, + esClient, + newPackagePolicies, + newAgentPolicy.id, + { + ...options, + bumpRevision: false, + } + ); } // Get updated agent policy @@ -307,15 +328,18 @@ class AgentPolicyService { public async bumpRevision( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, options?: { user?: AuthenticatedUser } ): Promise { - const res = await this._update(soClient, id, {}, options?.user); + const res = await this._update(soClient, esClient, id, {}, options?.user); return res; } + public async bumpAllAgentPolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options?: { user?: AuthenticatedUser } ): Promise>> { const currentPolicies = await soClient.find({ @@ -335,7 +359,7 @@ class AgentPolicyService { await Promise.all( currentPolicies.saved_objects.map((policy) => - this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', policy.id) + this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', policy.id) ) ); @@ -344,6 +368,7 @@ class AgentPolicyService { public async assignPackagePolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], options: { user?: AuthenticatedUser; bumpRevision: boolean } = { bumpRevision: true } @@ -356,6 +381,7 @@ class AgentPolicyService { return await this._update( soClient, + esClient, id, { package_policies: uniq( @@ -369,6 +395,7 @@ class AgentPolicyService { public async unassignPackagePolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], options?: { user?: AuthenticatedUser } @@ -381,6 +408,7 @@ class AgentPolicyService { return await this._update( soClient, + esClient, id, { package_policies: uniq( @@ -409,6 +437,7 @@ class AgentPolicyService { public async delete( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string ): Promise { const agentPolicy = await this.get(soClient, id, false); @@ -418,12 +447,12 @@ class AgentPolicyService { const { defaultAgentPolicy: { id: defaultAgentPolicyId }, - } = await this.ensureDefaultAgentPolicy(soClient); + } = await this.ensureDefaultAgentPolicy(soClient, esClient); if (id === defaultAgentPolicyId) { throw new Error('The default agent policy cannot be deleted'); } - const { total } = await listAgents(soClient, { + const { total } = await listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, @@ -435,12 +464,17 @@ class AgentPolicyService { } if (agentPolicy.package_policies && agentPolicy.package_policies.length) { - await packagePolicyService.delete(soClient, agentPolicy.package_policies as string[], { - skipUnassignFromAgentPolicies: true, - }); + await packagePolicyService.delete( + soClient, + esClient, + agentPolicy.package_policies as string[], + { + skipUnassignFromAgentPolicies: true, + } + ); } await soClient.delete(SAVED_OBJECT_TYPE, id); - await this.triggerAgentPolicyUpdatedEvent(soClient, 'deleted', id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'deleted', id); return { id, name: agentPolicy.name, @@ -450,6 +484,19 @@ class AgentPolicyService { public async createFleetPolicyChangeAction( soClient: SavedObjectsClientContract, agentPolicyId: string + ) { + return appContextService.getConfig()?.agents.fleetServerEnabled + ? this.createFleetPolicyChangeFleetServer( + soClient, + appContextService.getInternalUserESClient(), + agentPolicyId + ) + : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + } + + public async createFleetPolicyChangeActionSO( + soClient: SavedObjectsClientContract, + agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions // the action will be created during the fleet setup @@ -478,6 +525,38 @@ class AgentPolicyService { }); } + public async createFleetPolicyChangeFleetServer( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentPolicyId: string + ) { + // If Agents is not setup skip the creation of POLICY_CHANGE agent actions + // the action will be created during the fleet setup + if (!(await isAgentsSetup(soClient))) { + return; + } + const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); + if (!policy || !policy.revision) { + return; + } + + const fleetServerPolicy: FleetServerPolicy = { + '@timestamp': new Date().toISOString(), + revision_idx: policy.revision, + coordinator_idx: 0, + data: (policy as unknown) as FleetServerPolicy['data'], + policy_id: policy.id, + default_fleet_server: false, + }; + + await esClient.create({ + index: AGENT_POLICY_INDEX, + body: fleetServerPolicy, + id: uuid(), + refresh: 'wait_for', + }); + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index fe06de765bbff..32c041b446818 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForAgentPolicyId } from './api_keys'; import { isAgentsSetup, unenrollForAgentPolicyId } from './agents'; import { agentPolicyService } from './agent_policy'; @@ -27,6 +27,7 @@ const fakeRequest = ({ export async function agentPolicyUpdateEventHandler( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, action: string, agentPolicyId: string ) { @@ -40,7 +41,7 @@ export async function agentPolicyUpdateEventHandler( const internalSoClient = appContextService.getInternalUserSOClient(fakeRequest); if (action === 'created') { - await generateEnrollmentAPIKey(soClient, { + await generateEnrollmentAPIKey(soClient, esClient, { agentPolicyId, }); await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); @@ -51,7 +52,7 @@ export async function agentPolicyUpdateEventHandler( } if (action === 'deleted') { - await unenrollForAgentPolicyId(soClient, agentPolicyId); + await unenrollForAgentPolicyId(soClient, esClient, agentPolicyId); await deleteEnrollmentApiKeyForAgentPolicyId(soClient, agentPolicyId); } } diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index 4b09fb93e01a1..1626df4fd02ca 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -5,7 +5,7 @@ */ import Boom from '@hapi/boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { Agent, @@ -19,6 +19,7 @@ import { acknowledgeAgentActions } from './acks'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -41,6 +42,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -59,6 +61,7 @@ describe('test agent acks services', () => { it('should update config field on the agent if a policy change is acknowledged with an agent without policy', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -85,6 +88,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -118,6 +122,7 @@ describe('test agent acks services', () => { it('should update config field on the agent if a policy change is acknowledged with a higher revision than the agent one', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -144,6 +149,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -178,6 +184,7 @@ describe('test agent acks services', () => { it('should not update config field on the agent if a policy change is acknowledged with a lower revision than the agent one', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -204,6 +211,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -226,6 +234,7 @@ describe('test agent acks services', () => { it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -249,6 +258,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -271,6 +281,7 @@ describe('test agent acks services', () => { it('should fail for actions that cannot be found on agent actions list', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ saved_objects: [ @@ -288,6 +299,7 @@ describe('test agent acks services', () => { try { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -310,6 +322,7 @@ describe('test agent acks services', () => { it('should fail for events that have types not in the allowed acknowledgement type list', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -333,6 +346,7 @@ describe('test agent acks services', () => { try { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index 814251345788e..fab6dae0d23d5 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -5,6 +5,7 @@ */ import { + ElasticsearchClient, KibanaRequest, SavedObjectsBulkCreateObject, SavedObjectsBulkResponse, @@ -40,6 +41,7 @@ const actionCache = new LRU({ export async function acknowledgeAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, agentEvents: AgentEvent[] ): Promise { @@ -79,7 +81,7 @@ export async function acknowledgeAgentActions( const isAgentUnenrolled = actions.some((action) => action.type === 'UNENROLL'); if (isAgentUnenrolled) { - await forceUnenrollAgent(soClient, agent.id); + await forceUnenrollAgent(soClient, esClient, agent.id); } const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); @@ -196,6 +198,7 @@ export async function saveAgentEvents( export interface AcksService { acknowledgeAgentActions: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, actionIds: AgentEvent[] ) => Promise; @@ -207,6 +210,8 @@ export interface AcksService { getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; + getElasticsearchClientContract: () => ElasticsearchClient; + saveAgentEvents: ( soClient: SavedObjectsClientContract, events: AgentEvent[] diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index f2cdd1f31e69f..cb893a8b88c98 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { Agent, AgentAction, @@ -307,7 +307,11 @@ export async function getLatestConfigChangeAction( } export interface ActionsService { - getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise; + getAgent: ( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string + ) => Promise; createAgentAction: ( soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 19a5c2dc08762..d9e7d9889efd9 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -5,7 +5,11 @@ */ import deepEqual from 'fast-deep-equal'; -import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { + ElasticsearchClient, + SavedObjectsClientContract, + SavedObjectsBulkCreateObject, +} from 'src/core/server'; import { Agent, NewAgentEvent, @@ -20,6 +24,7 @@ import { getAgentActionsForCheckin } from '../actions'; export async function agentCheckin( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, data: { events: NewAgentEvent[]; @@ -54,7 +59,7 @@ export async function agentCheckin( } // Wait for new actions - actions = await agentCheckinState.subscribeToNewActions(soClient, agent, options); + actions = await agentCheckinState.subscribeToNewActions(soClient, esClient, agent, options); return { actions }; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state.ts index 63f22b82611c2..bdbf391650bc7 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { Agent } from '../../../types'; import { appContextService } from '../../app_context'; import { agentCheckinStateConnectedAgentsFactory } from './state_connected_agents'; @@ -35,6 +35,7 @@ function agentCheckinStateFactory() { return { subscribeToNewActions: async ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, options?: { signal: AbortSignal } ) => { @@ -44,7 +45,7 @@ function agentCheckinStateFactory() { return agentConnected.wrapPromise( agent.id, - newActions.subscribeToNewActions(soClient, agent, options) + newActions.subscribeToNewActions(soClient, esClient, agent, options) ); }, start, diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 59887d223371f..0d5394a88a87b 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -21,7 +21,7 @@ import { timeout, take, } from 'rxjs/operators'; -import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { Agent, AgentAction, @@ -228,6 +228,7 @@ export function agentCheckinStateNewActionsFactory() { async function subscribeToNewActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, options?: { signal: AbortSignal } ): Promise { @@ -262,7 +263,7 @@ export function agentCheckinStateNewActionsFactory() { (action) => action.type === 'INTERNAL_POLICY_REASSIGN' ); if (hasConfigReassign) { - return from(getAgent(soClient, agent.id)).pipe( + return from(getAgent(soClient, esClient, agent.id)).pipe( concatMap((refreshedAgent) => { if (!refreshedAgent.policy_id) { throw new Error('Agent does not have a policy assigned'); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index bcd409e5f7eab..58f64c65e081d 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -4,29 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { isAgentUpgradeable } from '../../../common'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; + +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase } from '../saved_object'; import { savedObjectToAgent } from './saved_objects'; import { appContextService } from '../../services'; - -const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; -const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; - -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } - - return `(${filter})`; - }, undefined); -} +import * as crudServiceSO from './crud_so'; +import * as crudServiceFleetServer from './crud_fleet_server'; export async function listAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: ListWithKuery & { showInactive: boolean; } @@ -36,52 +26,16 @@ export async function listAgents( page: number; perPage: number; }> { - const { - page = 1, - perPage = 20, - sortField = 'enrolled_at', - sortOrder = 'desc', - kuery, - showInactive = false, - showUpgradeable, - } = options; - const filters = []; - - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - let { saved_objects: agentSOs, total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters), - sortField, - sortOrder, - page, - perPage, - }); - // filtering for a range on the version string will not work, - // nor does filtering on a flattened field (local_metadata), so filter here - if (showUpgradeable) { - agentSOs = agentSOs.filter((agent) => - isAgentUpgradeable(savedObjectToAgent(agent), appContextService.getKibanaVersion()) - ); - total = agentSOs.length; - } - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - page, - perPage, - }; + return fleetServerEnabled + ? crudServiceFleetServer.listAgents(esClient, options) + : crudServiceSO.listAgents(soClient, options); } export async function listAllAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: Omit & { showInactive: boolean; } @@ -89,55 +43,34 @@ export async function listAllAgents( agents: Agent[]; total: number; }> { - const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; - const filters = []; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } - - const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { - type: AGENT_SAVED_OBJECT_TYPE, - kuery: _joinFilters(filters), - sortField, - sortOrder, - }); - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - }; + return fleetServerEnabled + ? crudServiceFleetServer.listAllAgents(esClient, options) + : crudServiceSO.listAllAgents(soClient, options); } export async function countInactiveAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: Pick ): Promise { - const { kuery } = options; - const filters = [INACTIVE_AGENT_CONDITION]; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - const { total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters), - perPage: 0, - }); - - return total; + return fleetServerEnabled + ? crudServiceFleetServer.countInactiveAgents(esClient, options) + : crudServiceSO.countInactiveAgents(soClient, options); } -export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = savedObjectToAgent( - await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) - ); - return agent; +export async function getAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgent(esClient, agentId) + : crudServiceSO.getAgent(soClient, agentId); } export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { @@ -187,31 +120,13 @@ export async function updateAgent( }); } -export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = await getAgent(soClient, agentId); - if (agent.type === 'EPHEMERAL') { - // Delete events - let more = true; - while (more === true) { - const { saved_objects: events } = await soClient.find({ - type: AGENT_EVENT_SAVED_OBJECT_TYPE, - fields: ['id'], - search: agentId, - searchFields: ['agent_id'], - perPage: 1000, - }); - if (events.length === 0) { - more = false; - } - for (const event of events) { - await soClient.delete(AGENT_EVENT_SAVED_OBJECT_TYPE, event.id); - } - } - await soClient.delete(AGENT_SAVED_OBJECT_TYPE, agentId); - return; - } - - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - active: false, - }); +export async function deleteAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.deleteAgent(esClient, agentId) + : crudServiceSO.deleteAgent(soClient, agentId); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts new file mode 100644 index 0000000000000..9c5e45c05de00 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Boom from '@hapi/boom'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; + +import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; +import { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; +import { savedObjectToAgent } from './saved_objects'; +import { searchHitToAgent } from './helpers'; +import { appContextService } from '../../services'; + +const ACTIVE_AGENT_CONDITION = 'active:true'; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} + +function removeSOAttributes(kuery: string) { + return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); +} + +export async function listAgents( + esClient: ElasticsearchClient, + options: ListWithKuery & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + showUpgradeable, + } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(removeSOAttributes(kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const res = await esClient.search({ + index: AGENTS_INDEX, + from: (page - 1) * perPage, + size: perPage, + sort: `${sortField}:${sortOrder}`, + track_total_hits: true, + q: _joinFilters(filters), + }); + + let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); + let total = res.body.hits.total.value; + + // filtering for a range on the version string will not work, + // nor does filtering on a flattened field (local_metadata), so filter here + if (showUpgradeable) { + agentResults = agentResults.filter((agent) => + isAgentUpgradeable(agent, appContextService.getKibanaVersion()) + ); + total = agentResults.length; + } + + return { + agents: res.body.hits.hits.map(searchHitToAgent), + total, + page, + perPage, + }; +} + +export async function listAllAgents( + esClient: ElasticsearchClient, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const res = await listAgents(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); + + return { + agents: res.agents, + total: res.total, + }; +} + +export async function countInactiveAgents( + esClient: ElasticsearchClient, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const res = await esClient.search({ + index: AGENTS_INDEX, + size: 0, + track_total_hits: true, + q: _joinFilters(filters), + }); + + return res.body.hits.total.value; +} + +export async function getAgent(esClient: ElasticsearchClient, agentId: string) { + const agentHit = await esClient.get>({ + index: AGENTS_INDEX, + id: agentId, + }); + const agent = searchHitToAgent(agentHit.body); + + return agent; +} + +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + +export async function getAgentByAccessAPIKeyId( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string +): Promise { + const response = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + searchFields: ['access_api_key_id'], + search: escapeSearchQueryPhrase(accessAPIKeyId), + }); + const [agent] = response.saved_objects.map(savedObjectToAgent); + + if (!agent) { + throw Boom.notFound('Agent not found'); + } + if (agent.access_api_key_id !== accessAPIKeyId) { + throw new Error('Agent api key id is not matching'); + } + if (!agent.active) { + throw Boom.forbidden('Agent inactive'); + } + + return agent; +} + +export async function updateAgent( + soClient: SavedObjectsClientContract, + agentId: string, + data: { + userProvidedMetatada: any; + } +) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + user_provided_metadata: data.userProvidedMetatada, + }); +} + +export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { + await esClient.update({ + id: agentId, + index: AGENT_SAVED_OBJECT_TYPE, + body: { + active: false, + }, + }); +} diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts new file mode 100644 index 0000000000000..eb8f389741a6a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Boom from '@hapi/boom'; +import { SavedObjectsClientContract } from 'src/core/server'; + +import { isAgentUpgradeable } from '../../../common'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; +import { savedObjectToAgent } from './saved_objects'; +import { appContextService } from '../../services'; + +const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} + +export async function listAgents( + soClient: SavedObjectsClientContract, + options: ListWithKuery & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + showUpgradeable, + } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + let { saved_objects: agentSOs, total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + sortField, + sortOrder, + page, + perPage, + }); + // filtering for a range on the version string will not work, + // nor does filtering on a flattened field (local_metadata), so filter here + if (showUpgradeable) { + agentSOs = agentSOs.filter((agent) => + isAgentUpgradeable(savedObjectToAgent(agent), appContextService.getKibanaVersion()) + ); + total = agentSOs.length; + } + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + page, + perPage, + }; +} + +export async function listAllAgents( + soClient: SavedObjectsClientContract, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { + type: AGENT_SAVED_OBJECT_TYPE, + kuery: _joinFilters(filters), + sortField, + sortOrder, + }); + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + }; +} + +export async function countInactiveAgents( + soClient: SavedObjectsClientContract, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const { total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + perPage: 0, + }); + + return total; +} + +export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { + const agent = savedObjectToAgent( + await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) + ); + return agent; +} + +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + +export async function getAgentByAccessAPIKeyId( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string +): Promise { + const response = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + searchFields: ['access_api_key_id'], + search: escapeSearchQueryPhrase(accessAPIKeyId), + }); + const [agent] = response.saved_objects.map(savedObjectToAgent); + + if (!agent) { + throw Boom.notFound('Agent not found'); + } + if (agent.access_api_key_id !== accessAPIKeyId) { + throw new Error('Agent api key id is not matching'); + } + if (!agent.active) { + throw Boom.forbidden('Agent inactive'); + } + + return agent; +} + +export async function updateAgent( + soClient: SavedObjectsClientContract, + agentId: string, + data: { + userProvidedMetatada: any; + } +) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + user_provided_metadata: data.userProvidedMetatada, + }); +} + +export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + active: false, + }); +} diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts new file mode 100644 index 0000000000000..38330a090ae81 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { Agent, AgentSOAttributes } from '../../types'; + +export function searchHitToAgent(hit: ESSearchHit): Agent { + return { + id: hit._id, + ...hit._source, + current_error_events: hit._source.current_error_events + ? JSON.parse(hit._source.current_error_events) + : [], + access_api_key: undefined, + status: undefined, + packages: hit._source.packages ?? [], + }; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index b656ab12e96c8..8a1dc61950885 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes } from '../../types'; @@ -14,6 +14,7 @@ import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, newAgentPolicyId: string ) { @@ -36,6 +37,7 @@ export async function reassignAgent( export async function reassignAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -55,7 +57,7 @@ export async function reassignAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) diff --git a/x-pack/plugins/fleet/server/services/agents/status.test.ts b/x-pack/plugins/fleet/server/services/agents/status.test.ts index f216cd541eb21..587f0af227ff8 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { getAgentStatusById } from './status'; import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; import { AgentSOAttributes } from '../../../common/types/models'; @@ -13,6 +13,7 @@ import { SavedObject } from 'kibana/server'; describe('Agent status service', () => { it('should return inactive when agent is not active', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -22,12 +23,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('inactive'); }); it('should return online when agent is active', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -38,12 +40,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('online'); }); it('should return enrolling when agent is active but never checkin', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -53,12 +56,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('enrolling'); }); it('should return unenrolling when agent is unenrolling', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -70,7 +74,7 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('unenrolling'); }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 74faedc8e2931..ba8f8fc363857 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import pMap from 'p-map'; import { getAgent, listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -14,9 +14,10 @@ import { AgentStatusKueryHelper } from '../../../common/services'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string ): Promise { - const agent = await getAgent(soClient, agentId); + const agent = await getAgent(soClient, esClient, agentId); return AgentStatusKueryHelper.getAgentStatus(agent); } @@ -36,6 +37,7 @@ function joinKuerys(...kuerys: Array) { export async function getAgentStatusForAgentPolicy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId?: string, filterKuery?: string ) { @@ -48,7 +50,7 @@ export async function getAgentStatusForAgentPolicy( AgentStatusKueryHelper.buildKueryForUpdatingAgents(), ], (kuery) => - listAgents(soClient, { + listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 9c2b2bdfe7f6d..5246927cb4ee4 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; @@ -25,6 +25,7 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI export async function unenrollAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -38,7 +39,7 @@ export async function unenrollAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) @@ -70,8 +71,12 @@ export async function unenrollAgents( ); } -export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = await getAgent(soClient, agentId); +export async function forceUnenrollAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const agent = await getAgent(soClient, esClient, agentId); await Promise.all([ agent.access_api_key_id @@ -90,6 +95,7 @@ export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, a export async function forceUnenrollAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -103,7 +109,7 @@ export async function forceUnenrollAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts index b85a831294b58..7bd807bf4e575 100644 --- a/x-pack/plugins/fleet/server/services/agents/update.ts +++ b/x-pack/plugins/fleet/server/services/agents/update.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { unenrollAgent } from './unenroll'; export async function unenrollForAgentPolicyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, policyId: string ) { let hasMore = true; let page = 1; while (hasMore) { - const { agents } = await listAgents(soClient, { + const { agents } = await listAgents(soClient, esClient, { kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${policyId}"`, page: page++, perPage: 1000, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index cf83a938d3c39..9515cca8ce007 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; @@ -59,6 +59,7 @@ export async function ackAgentUpgraded( export async function sendUpgradeAgentsActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -79,7 +80,7 @@ export async function sendUpgradeAgentsActions( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 8f67753392e65..747cbae3f71ce 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; -import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; -import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { createAPIKey, invalidateAPIKeys } from './security'; -import { agentPolicyService } from '../agent_policy'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { EnrollmentAPIKey } from '../../types'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import * as enrollmentApiKeyServiceSO from './enrollment_api_key_so'; +import * as enrollmentApiKeyServiceFleetServer from './enrollment_api_key_fleet_server'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: { page?: number; perPage?: number; @@ -23,39 +20,23 @@ export async function listEnrollmentApiKeys( showInactive?: boolean; } ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { - const { page = 1, perPage = 20, kuery } = options; - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects, total } = await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - page, - perPage, - sortField: 'created_at', - sortOrder: 'desc', - filter: - kuery && kuery !== '' - ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) - : undefined, - }); - - const items = saved_objects.map(savedObjectToEnrollmentApiKey); - - return { - items, - total, - page, - perPage, - }; + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.listEnrollmentApiKeys(esClient, options); + } else { + return enrollmentApiKeyServiceSO.listEnrollmentApiKeys(soClient, options); + } } -export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - const so = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser( - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - id - ); - return savedObjectToEnrollmentApiKey(so); +export async function getEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKey(esClient, id); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKey(soClient, id); + } } /** @@ -63,112 +44,37 @@ export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, * @param soClient * @param id */ -export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { - const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); - - await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); - - await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { - active: false, - }); +export async function deleteEnrollmentApiKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.deleteEnrollmentApiKey(soClient, esClient, id); + } else { + return enrollmentApiKeyServiceSO.deleteEnrollmentApiKey(soClient, id); + } } export async function deleteEnrollmentApiKeyForAgentPolicyId( soClient: SavedObjectsClientContract, agentPolicyId: string ) { - let hasMore = true; - let page = 1; - while (hasMore) { - const { items } = await listEnrollmentApiKeys(soClient, { - page: page++, - perPage: 100, - kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`, - }); - - if (items.length === 0) { - hasMore = false; - } - - for (const apiKey of items) { - await deleteEnrollmentApiKey(soClient, apiKey.id); - } - } + return enrollmentApiKeyServiceSO.deleteEnrollmentApiKeyForAgentPolicyId(soClient, agentPolicyId); } export async function generateEnrollmentAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, data: { name?: string; expiration?: string; agentPolicyId?: string; } ) { - const id = uuid.v4(); - const { name: providedKeyName } = data; - if (data.agentPolicyId) { - await validateAgentPolicyId(soClient, data.agentPolicyId); - } - const agentPolicyId = - data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); - const name = providedKeyName ? `${providedKeyName} (${id})` : id; - const key = await createAPIKey(soClient, name, { - // Useless role to avoid to have the privilege of the user that created the key - 'fleet-apikey-enroll': { - cluster: [], - applications: [ - { - application: '.fleet', - privileges: ['no-privileges'], - resources: ['*'], - }, - ], - }, - }); - - if (!key) { - throw new Error('Unable to create an enrollment api key'); + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.generateEnrollmentAPIKey(soClient, esClient, data); + } else { + return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } - - const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); - - const so = await soClient.create( - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - { - active: true, - api_key_id: key.id, - api_key: apiKey, - name, - policy_id: agentPolicyId, - created_at: new Date().toISOString(), - } - ); - - return getEnrollmentAPIKey(soClient, so.id); -} - -async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { - try { - await agentPolicyService.get(soClient, agentPolicyId); - } catch (e) { - if (e.isBoom && e.output.statusCode === 404) { - throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); - } - throw e; - } -} - -function savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; } diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts new file mode 100644 index 0000000000000..c0aa42c6e4ed8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import Boom from '@hapi/boom'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; +import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; +import { createAPIKey, invalidateAPIKeys } from './security'; +import { agentPolicyService } from '../agent_policy'; + +// TODO Move these types to another file +interface SearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + hits: { + total: { + value: number; + relation: string; + }; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; +} + +type SearchHit = SearchResponse['hits']['hits'][0]; + +export async function listEnrollmentApiKeys( + esClient: ElasticsearchClient, + options: { + page?: number; + perPage?: number; + kuery?: string; + showInactive?: boolean; + } +): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { + const { page = 1, perPage = 20, kuery } = options; + + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + from: (page - 1) * perPage, + size: perPage, + sort: 'created_at:desc', + track_total_hits: true, + q: kuery, + }); + + const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + return { + items, + total: res.body.hits.total.value, + page, + perPage, + }; +} + +export async function getEnrollmentAPIKey( + esClient: ElasticsearchClient, + id: string +): Promise { + try { + const res = await esClient.get>({ + index: ENROLLMENT_API_KEYS_INDEX, + id, + }); + + return esDocToEnrollmentApiKey(res.body); + } catch (e) { + if (e instanceof ResponseError && e.statusCode === 404) { + throw Boom.notFound(`Enrollment api key ${id} not found`); + } + + throw e; + } +} + +/** + * Invalidate an api key and mark it as inactive + * @param soClient + * @param id + */ +export async function deleteEnrollmentApiKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id); + + await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); + + await esClient.update({ + index: ENROLLMENT_API_KEYS_INDEX, + id, + body: { + doc: { + active: false, + }, + }, + refresh: 'wait_for', + }); +} + +export async function deleteEnrollmentApiKeyForAgentPolicyId( + soClient: SavedObjectsClientContract, + agentPolicyId: string +) { + throw new Error('NOT IMPLEMENTED'); +} + +export async function generateEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: { + name?: string; + expiration?: string; + agentPolicyId?: string; + } +): Promise { + const id = uuid.v4(); + const { name: providedKeyName } = data; + if (data.agentPolicyId) { + await validateAgentPolicyId(soClient, data.agentPolicyId); + } + const agentPolicyId = + data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); + const name = providedKeyName ? `${providedKeyName} (${id})` : id; + const key = await createAPIKey(soClient, name, { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }); + + if (!key) { + throw new Error('Unable to create an enrollment api key'); + } + + const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); + + const body = { + active: true, + api_key_id: key.id, + api_key: apiKey, + name, + policy_id: agentPolicyId, + created_at: new Date().toISOString(), + }; + + const res = await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id, + refresh: 'wait_for', + }); + + return { + id: res.body._id, + ...body, + }; +} + +async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { + try { + await agentPolicyService.get(soClient, agentPolicyId); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); + } + throw e; + } +} + +function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { + return { + id: doc._id, + ...doc._source, + created_at: doc._source.created_at as string, + active: doc._source.active || false, + }; +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts new file mode 100644 index 0000000000000..8f67753392e65 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import Boom from '@hapi/boom'; +import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; +import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; +import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { createAPIKey, invalidateAPIKeys } from './security'; +import { agentPolicyService } from '../agent_policy'; +import { appContextService } from '../app_context'; +import { normalizeKuery } from '../saved_object'; + +export async function listEnrollmentApiKeys( + soClient: SavedObjectsClientContract, + options: { + page?: number; + perPage?: number; + kuery?: string; + showInactive?: boolean; + } +): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { + const { page = 1, perPage = 20, kuery } = options; + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { saved_objects, total } = await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + page, + perPage, + sortField: 'created_at', + sortOrder: 'desc', + filter: + kuery && kuery !== '' + ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) + : undefined, + }); + + const items = saved_objects.map(savedObjectToEnrollmentApiKey); + + return { + items, + total, + page, + perPage, + }; +} + +export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { + const so = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + id + ); + return savedObjectToEnrollmentApiKey(so); +} + +/** + * Invalidate an api key and mark it as inactive + * @param soClient + * @param id + */ +export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { + const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); + + await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); + + await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { + active: false, + }); +} + +export async function deleteEnrollmentApiKeyForAgentPolicyId( + soClient: SavedObjectsClientContract, + agentPolicyId: string +) { + let hasMore = true; + let page = 1; + while (hasMore) { + const { items } = await listEnrollmentApiKeys(soClient, { + page: page++, + perPage: 100, + kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`, + }); + + if (items.length === 0) { + hasMore = false; + } + + for (const apiKey of items) { + await deleteEnrollmentApiKey(soClient, apiKey.id); + } + } +} + +export async function generateEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + data: { + name?: string; + expiration?: string; + agentPolicyId?: string; + } +) { + const id = uuid.v4(); + const { name: providedKeyName } = data; + if (data.agentPolicyId) { + await validateAgentPolicyId(soClient, data.agentPolicyId); + } + const agentPolicyId = + data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); + const name = providedKeyName ? `${providedKeyName} (${id})` : id; + const key = await createAPIKey(soClient, name, { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }); + + if (!key) { + throw new Error('Unable to create an enrollment api key'); + } + + const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); + + const so = await soClient.create( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + { + active: true, + api_key_id: key.id, + api_key: apiKey, + name, + policy_id: agentPolicyId, + created_at: new Date().toISOString(), + } + ); + + return getEnrollmentAPIKey(soClient, so.id); +} + +async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { + try { + await agentPolicyService.get(soClient, agentPolicyId); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); + } + throw e; + } +} + +function savedObjectToEnrollmentApiKey({ + error, + attributes, + id, +}: SavedObject): EnrollmentAPIKey { + if (error) { + throw new Error(error.message); + } + + return { + id, + ...attributes, + }; +} diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index d6b62458ed1f4..66ffd3ca53081 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -5,7 +5,13 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart, HttpServiceSetup, Logger, KibanaRequest } from 'src/core/server'; +import { + ElasticsearchClient, + SavedObjectsServiceStart, + HttpServiceSetup, + Logger, + KibanaRequest, +} from 'src/core/server'; import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, @@ -19,6 +25,7 @@ import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; + private esClient: ElasticsearchClient | undefined; private security: SecurityPluginStart | undefined; private config$?: Observable; private configSubject$?: BehaviorSubject; @@ -32,6 +39,7 @@ class AppContextService { private externalCallbacks: ExternalCallbacksStorage = new Map(); public async start(appContext: FleetAppContext) { + this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); this.encryptedSavedObjectsSetup = appContext.encryptedSavedObjectsSetup; this.security = appContext.security; @@ -96,12 +104,20 @@ class AppContextService { } public getInternalUserSOClient(request: KibanaRequest) { - // soClient as kibana internal users, be carefull on how you use it, security is not enabled + // soClient as kibana internal users, be careful on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(request, { excludedWrappers: ['security'], }); } + public getInternalUserESClient() { + if (!this.esClient) { + throw new Error('Elasticsearch start service not set.'); + } + // soClient as kibana internal users, be careful on how you use it, security is not enabled + return this.esClient; + } + public getIsProductionMode() { return this.isProductionMode; } diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts new file mode 100644 index 0000000000000..1a50b5c9df767 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'src/core/server'; +import { + ENROLLMENT_API_KEYS_INDEX, + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + FleetServerEnrollmentAPIKey, +} from '../../common'; +import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollment_api_key_so'; +import { appContextService } from './app_context'; + +export async function runFleetServerMigration() { + const logger = appContextService.getLogger(); + logger.info('Starting fleet server migration'); + await migrateEnrollmentApiKeys(); + logger.info('Fleet server migration finished'); +} + +function getInternalUserSOClient() { + const fakeRequest = ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown) as KibanaRequest; + + return appContextService.getInternalUserSOClient(fakeRequest); +} + +async function migrateEnrollmentApiKeys() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await listEnrollmentApiKeys(soClient, { + page: 1, + perPage: 100, + }); + if (res.total === 0) { + hasMore = false; + } + for (const item of res.items) { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); + + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index d9015c5195536..b590b2ed002c0 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; import { AgentStatus, Agent, EsAssetReference } from '../types'; import * as settingsService from './settings'; import { getAgent, listAgents } from './agents'; @@ -53,7 +53,11 @@ export interface AgentService { /** * Return the status by the Agent's id */ - getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; + getAgentStatusById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string + ): Promise; /** * List agents */ diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 5e295c1576705..eb26b405fbdab 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackagePolicyMock } from '../../common/mocks'; import { packagePolicyService } from './package_policy'; import { PackageInfo, PackagePolicySOAttributes } from '../types'; @@ -345,9 +345,11 @@ describe('Package policy service', () => { throw savedObjectsClient.errors.createConflictError('abc', '123'); } ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; await expect( packagePolicyService.update( savedObjectsClient, + elasticsearchClient, 'the-package-policy-id', createPackagePolicyMock() ) diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 95b1a43ec2e5e..605b0f6cf65cc 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; +import { + ElasticsearchClient, + KibanaRequest, + RequestHandlerContext, + SavedObjectsClientContract, +} from 'src/core/server'; import uuid from 'uuid'; import { AuthenticatedUser } from '../../../security/server'; import { @@ -47,6 +52,7 @@ function getDataset(st: string) { class PackagePolicyService { public async create( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, packagePolicy: NewPackagePolicy, options?: { id?: string; user?: AuthenticatedUser; bumpRevision?: boolean } @@ -116,10 +122,16 @@ class PackagePolicyService { ); // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies(soClient, packagePolicy.policy_id, [newSo.id], { - user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - }); + await agentPolicyService.assignPackagePolicies( + soClient, + esClient, + packagePolicy.policy_id, + [newSo.id], + { + user: options?.user, + bumpRevision: options?.bumpRevision ?? true, + } + ); return { id: newSo.id, @@ -130,6 +142,7 @@ class PackagePolicyService { public async bulkCreate( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, packagePolicies: NewPackagePolicy[], agentPolicyId: string, options?: { user?: AuthenticatedUser; bumpRevision?: boolean } @@ -167,6 +180,7 @@ class PackagePolicyService { // Assign it to the given agent policy await agentPolicyService.assignPackagePolicies( soClient, + esClient, agentPolicyId, newSos.map((newSo) => newSo.id), { @@ -252,6 +266,7 @@ class PackagePolicyService { public async update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicy: UpdatePackagePolicy, options?: { user?: AuthenticatedUser } @@ -308,7 +323,7 @@ class PackagePolicyService { ); // Bump revision of associated agent policy - await agentPolicyService.bumpRevision(soClient, packagePolicy.policy_id, { + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { user: options?.user, }); @@ -317,6 +332,7 @@ class PackagePolicyService { public async delete( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, ids: string[], options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean } ): Promise { @@ -331,6 +347,7 @@ class PackagePolicyService { if (!options?.skipUnassignFromAgentPolicies) { await agentPolicyService.unassignPackagePolicies( soClient, + esClient, packagePolicy.policy_id, [packagePolicy.id], { diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index bb01862aaf317..1e2440ba3b546 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -41,8 +41,9 @@ describe('setupIngestManager', () => { soClient.find = mockedMethodThrowsError(); soClient.get = mockedMethodThrowsError(); soClient.update = mockedMethodThrowsError(); + const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, jest.fn()); + const setupPromise = setupIngestManager(soClient, esClient, jest.fn()); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); await expect(setupPromise).rejects.toThrow(Error); }); @@ -53,8 +54,9 @@ describe('setupIngestManager', () => { soClient.find = mockedMethodThrowsCustom(); soClient.get = mockedMethodThrowsCustom(); soClient.update = mockedMethodThrowsCustom(); + const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, jest.fn()); + const setupPromise = setupIngestManager(soClient, esClient, jest.fn()); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index c37eed1910883..1ce7b1d85c8e4 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; import { agentPolicyService } from './agent_policy'; import { outputService } from './output'; @@ -39,13 +39,15 @@ export interface SetupStatus { export async function setupIngestManager( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser ): Promise { - return awaitIfPending(async () => createSetupSideEffects(soClient, callCluster)); + return awaitIfPending(async () => createSetupSideEffects(soClient, esClient, callCluster)); } async function createSetupSideEffects( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser ): Promise { const [ @@ -56,7 +58,7 @@ async function createSetupSideEffects( // packages installed by default ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), - agentPolicyService.ensureDefaultAgentPolicy(soClient), + agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); @@ -109,6 +111,7 @@ async function createSetupSideEffects( if (!isInstalled) { await addPackageToAgentPolicy( soClient, + esClient, callCluster, installedPackage, agentPolicyWithPackagePolicies, @@ -125,6 +128,7 @@ async function createSetupSideEffects( export async function setupFleet( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, options?: { forceRecreate?: boolean } ) { @@ -189,7 +193,7 @@ export async function setupFleet( await Promise.all( agentPolicies.map((agentPolicy) => { - return generateEnrollmentAPIKey(soClient, { + return generateEnrollmentAPIKey(soClient, esClient, { name: `Default`, agentPolicyId: agentPolicy.id, }); @@ -209,6 +213,7 @@ function generateRandomPassword() { async function addPackageToAgentPolicy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, packageToInstall: Installation, agentPolicy: AgentPolicy, @@ -227,7 +232,7 @@ async function addPackageToAgentPolicy( agentPolicy.namespace ); - await packagePolicyService.create(soClient, callCluster, newPackagePolicy, { + await packagePolicyService.create(soClient, esClient, callCluster, newPackagePolicy, { bumpRevision: false, }); } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 7e6e6d5e408b4..d3ac402159178 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -77,6 +77,8 @@ export { PostAgentCheckinRequest, DataType, dataTypes, + // Fleet Server types + FleetServerEnrollmentAPIKey, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 5773b88fa2bea..225592fa8e686 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -5,7 +5,11 @@ */ import { Subject } from 'rxjs'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsServiceMock, +} from 'src/core/server/mocks'; import { LicenseService } from '../../../../common/license/license'; import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; import { PolicyWatcher } from './license_watch'; @@ -31,6 +35,7 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); const soStartMock = savedObjectsServiceMock.createStartContract(); + const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -45,7 +50,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -90,7 +95,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -119,14 +124,14 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); expect(packagePolicySvcMock.update).toHaveBeenCalled(); expect( - packagePolicySvcMock.update.mock.calls[0][2].inputs[0].config!.policy.value.windows.popup + packagePolicySvcMock.update.mock.calls[0][3].inputs[0].config!.policy.value.windows.popup .malware.message ).not.toEqual(CustomMessage); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index 2f0c3bf8fd5ba..a8aa0f25b0782 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -7,6 +7,8 @@ import { Subscription } from 'rxjs'; import { + ElasticsearchClient, + ElasticsearchServiceStart, KibanaRequest, Logger, SavedObjectsClientContract, @@ -28,15 +30,18 @@ import { isAtLeast, LicenseService } from '../../../../common/license/license'; export class PolicyWatcher { private logger: Logger; private soClient: SavedObjectsClientContract; + private esClient: ElasticsearchClient; private policyService: PackagePolicyServiceInterface; private subscription: Subscription | undefined; constructor( policyService: PackagePolicyServiceInterface, soStart: SavedObjectsServiceStart, + esStart: ElasticsearchServiceStart, logger: Logger ) { this.policyService = policyService; this.soClient = this.makeInternalSOClient(soStart); + this.esClient = esStart.client.asInternalUser; this.logger = logger; } @@ -113,11 +118,16 @@ export class PolicyWatcher { license ); try { - await this.policyService.update(this.soClient, policy.id, updatePolicy); + await this.policyService.update(this.soClient, this.esClient, policy.id, updatePolicy); } catch (e) { // try again for transient issues try { - await this.policyService.update(this.soClient, policy.id, updatePolicy); + await this.policyService.update( + this.soClient, + this.esClient, + policy.id, + updatePolicy + ); } catch (ee) { this.logger.warn( `Unable to remove platinum features from policy ${policy.id}: ${ee.message}` diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index a79175b178c38..83732170fb5c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -68,13 +68,15 @@ export const getMetadataListRequestHandler = function ( const unenrolledAgentIds = await findAllUnenrolledAgentIds( agentService, - context.core.savedObjects.client + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser ); const statusIDs = request?.body?.filters?.host_status?.length ? await findAgentIDsByStatus( agentService, context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, request.body?.filters?.host_status ) : undefined; @@ -193,6 +195,7 @@ async function findAgent( ?.getAgentService() ?.getAgent( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, hostMetadata.elastic.agent.id ); } catch (e) { @@ -267,6 +270,7 @@ export async function enrichHostMetadata( ?.getAgentService() ?.getAgentStatusById( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, elasticAgentId ); hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.ERROR; @@ -289,6 +293,7 @@ export async function enrichHostMetadata( ?.getAgentService() ?.getAgent( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, elasticAgentId ); const agentPolicy = await metadataRequestContext.endpointAppContextService diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts index e9a1f1e24fa55..d7fe8b75cbc81 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { findAgentIDsByStatus } from './agent_status'; -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; @@ -14,9 +17,11 @@ import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services' describe('test filtering endpoint hosts by agent status', () => { let mockSavedObjectClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; let mockAgentService: jest.Mocked; beforeEach(() => { mockSavedObjectClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockAgentService = createMockAgentService(); }); @@ -30,7 +35,12 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['online'] + ); expect(result).toBeDefined(); }); @@ -53,9 +63,14 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['offline'] + ); const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentService.listAgents.mock.calls[0][2].kuery).toEqual( expect.stringContaining(offlineKuery) ); expect(result).toBeDefined(); @@ -81,13 +96,15 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ - 'unenrolling', - 'error', - ]); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['unenrolling', 'error'] + ); const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentService.listAgents.mock.calls[0][2].kuery).toEqual( expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) ); expect(result).toBeDefined(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts index 395b05c0887e9..4d3fd806dc635 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { AgentService } from '../../../../../../fleet/server'; import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services'; import { Agent } from '../../../../../../fleet/common/types/models'; @@ -20,6 +20,7 @@ const STATUS_QUERY_MAP = new Map([ export async function findAgentIDsByStatus( agentService: AgentService, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, status: string[], pageSize: number = 1000 ): Promise { @@ -39,7 +40,7 @@ export async function findAgentIDsByStatus( let hasMore = true; while (hasMore) { - const agents = await agentService.listAgents(soClient, searchOptions(page++)); + const agents = await agentService.listAgents(soClient, esClient, searchOptions(page++)); result.push(...agents.agents.map((agent: Agent) => agent.id)); hasMore = agents.agents.length > 0; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts index c88f11422d0f0..ea68f6270e730 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -4,18 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { findAllUnenrolledAgentIds } from './unenroll'; -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; describe('test find all unenrolled Agent id', () => { let mockSavedObjectClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; let mockAgentService: jest.Mocked; beforeEach(() => { mockSavedObjectClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockAgentService = createMockAgentService(); }); @@ -53,7 +58,11 @@ describe('test find all unenrolled Agent id', () => { perPage: 1, }) ); - const agentIds = await findAllUnenrolledAgentIds(mockAgentService, mockSavedObjectClient); + const agentIds = await findAllUnenrolledAgentIds( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient + ); expect(agentIds).toBeTruthy(); expect(agentIds).toEqual(['id1', 'id2']); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index 1abea86c1a495..45664f087f1b2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { AgentService } from '../../../../../../fleet/server'; import { Agent } from '../../../../../../fleet/common/types/models'; export async function findAllUnenrolledAgentIds( agentService: AgentService, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, pageSize: number = 1000 ): Promise { const searchOptions = (pageNum: number) => { @@ -29,7 +30,11 @@ export async function findAllUnenrolledAgentIds( let hasMore = true; while (hasMore) { - const unenrolledAgents = await agentService.listAgents(soClient, searchOptions(page++)); + const unenrolledAgents = await agentService.listAgents( + soClient, + esClient, + searchOptions(page++) + ); result.push(...unenrolledAgents.agents.map((agent: Agent) => agent.id)); hasMore = unenrolledAgents.agents.length > 0; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index 728e3279c52a4..46b67706c99ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -44,6 +44,7 @@ export const getAgentPolicySummaryHandler = function ( const result = await getAgentPolicySummary( endpointAppContext, context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, request.query.package_name, request.query?.policy_id || undefined ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index dd4ade1906bc6..f52535053a531 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -5,7 +5,11 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { + ElasticsearchClient, + ILegacyScopedClusterClient, + SavedObjectsClientContract, +} from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; import { Agent } from '../../../../../fleet/common/types/models'; @@ -73,6 +77,7 @@ const transformAgentVersionMap = (versionMap: Map): { [key: stri export async function getAgentPolicySummary( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, packageName: string, policyId?: string, pageSize: number = 1000 @@ -83,6 +88,7 @@ export async function getAgentPolicySummary( await agentVersionsMap( endpointAppContext, soClient, + esClient, `${agentQuery} AND ${AGENT_SAVED_OBJECT_TYPE}.policy_id:${policyId}`, pageSize ) @@ -90,13 +96,14 @@ export async function getAgentPolicySummary( } return transformAgentVersionMap( - await agentVersionsMap(endpointAppContext, soClient, agentQuery, pageSize) + await agentVersionsMap(endpointAppContext, soClient, esClient, agentQuery, pageSize) ); } export async function agentVersionsMap( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, kqlQuery: string, pageSize: number = 1000 ): Promise> { @@ -115,7 +122,7 @@ export async function agentVersionsMap( while (hasMore) { const queryResult = await endpointAppContext.service .getAgentService()! - .listAgents(soClient, searchOptions(page++)); + .listAgents(soClient, esClient, searchOptions(page++)); queryResult.agents.forEach((agent: Agent) => { const agentVersion = agent.local_metadata?.elastic?.agent?.version; if (result.has(agentVersion)) { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 9cb3f3d20543c..2bd4fcf348f76 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -191,7 +191,7 @@ describe('manifest_manager', () => { expect(packagePolicyService.update.mock.calls.length).toEqual(2); expect( - packagePolicyService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value + packagePolicyService.update.mock.calls[0][3].inputs[0].config!.artifact_manifest.value ).toEqual({ manifest_version: '1.0.1', schema_version: 'v1', diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 9f45f39a392f6..44a0fb15e48d9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -293,7 +293,13 @@ export class ManifestManager { }; try { - await this.packagePolicyService.update(this.savedObjectsClient, id, newPackagePolicy); + await this.packagePolicyService.update( + this.savedObjectsClient, + // @ts-ignore + undefined, + id, + newPackagePolicy + ); this.logger.debug( `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}` ); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index d51346ee9645a..020237ad497f7 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -355,6 +355,7 @@ export class Plugin implements IPlugin Date: Wed, 20 Jan 2021 21:15:47 -0600 Subject: [PATCH 26/28] [Workplace Search] Add tests for Custom Source Schema (#88785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add tests for Schema components * Convert components to use clearFlashMessages helper * Remove unused action types These aren’t actually used anymore * Fix type This is actually a string from the server * Move mock to shared mocks * Add tests for logic file * Fix App Search tests Server actually sends back a string for `activeReindexJobId` Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/engine/engine_logic.test.ts | 2 +- .../indexing_status_logic.test.ts | 2 +- .../public/applications/shared/types.ts | 2 +- .../__mocks__/content_sources.mock.ts | 8 + .../components/schema/schema.test.tsx | 125 +++++ .../schema/schema_change_errors.test.tsx | 36 ++ .../schema/schema_fields_table.test.tsx | 41 ++ .../components/schema/schema_fields_table.tsx | 2 +- .../components/schema/schema_logic.test.ts | 470 ++++++++++++++++++ .../components/schema/schema_logic.ts | 12 +- .../views/content_sources/source_logic.ts | 6 +- .../views/content_sources/sources_logic.ts | 4 +- .../views/content_sources/sources_view.tsx | 4 +- 13 files changed, 696 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 62f444cf8f6ab..48cbaeef70c1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -89,7 +89,7 @@ describe('EngineLogic', () => { const mockReindexJob = { percentageComplete: 50, numDocumentsWithErrors: 2, - activeReindexJobId: 123, + activeReindexJobId: '123', }; EngineLogic.actions.setIndexingStatus(mockReindexJob); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts index 558271a8fbdc6..0a80f8e361025 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts @@ -21,7 +21,7 @@ describe('IndexingStatusLogic', () => { const mockStatusResponse = { percentageComplete: 50, numDocumentsWithErrors: 3, - activeReindexJobId: 1, + activeReindexJobId: '1', }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index f5833a0ac9f8e..0ad5f292ebed7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -33,7 +33,7 @@ export interface SchemaConflicts { export interface IIndexingStatus { percentageComplete: number; numDocumentsWithErrors: number; - activeReindexJobId: number; + activeReindexJobId: string; } export interface IndexJob extends IIndexingStatus { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index b3962a7c88cc8..3cd84d90d9a86 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -258,3 +258,11 @@ export const exampleResult = { }, ], }; + +export const mostRecentIndexJob = { + isActive: true, + hasErrors: true, + percentageComplete: 50, + activeReindexJobId: '123', + numDocumentsWithErrors: 1, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx new file mode 100644 index 0000000000000..1d7a73970bf1c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; + +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; + +import { IndexingStatus } from '../../../../../shared/indexing_status'; +import { Loading } from '../../../../../shared/loading'; +import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; + +import { SchemaFieldsTable } from './schema_fields_table'; + +import { Schema } from './schema'; + +describe('Schema', () => { + const initializeSchema = jest.fn(); + const onIndexingComplete = jest.fn(); + const addNewField = jest.fn(); + const updateFields = jest.fn(); + const openAddFieldModal = jest.fn(); + const closeAddFieldModal = jest.fn(); + const setFilterValue = jest.fn(); + + const sourceId = '123'; + const activeSchema = { + foo: 'string', + }; + const filterValue = ''; + const showAddFieldModal = false; + const addFieldFormErrors = null; + const formUnchanged = true; + const dataLoading = false; + const isOrganization = true; + + const mockValues = { + sourceId, + activeSchema, + filterValue, + showAddFieldModal, + addFieldFormErrors, + mostRecentIndexJob: {}, + formUnchanged, + dataLoading, + isOrganization, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + setMockActions({ + initializeSchema, + onIndexingComplete, + addNewField, + updateFields, + openAddFieldModal, + closeAddFieldModal, + setFilterValue, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SchemaFieldsTable)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('handles empty state', () => { + setMockValues({ ...mockValues, activeSchema: {} }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('renders modal', () => { + setMockValues({ ...mockValues, showAddFieldModal: true }); + const wrapper = shallow(); + + expect(wrapper.find(SchemaAddFieldModal)).toHaveLength(1); + }); + + it('gets search results when filters changed', () => { + const wrapper = shallow(); + const input = wrapper.find(EuiFieldSearch); + input.simulate('change', { target: { value: 'Query' } }); + + expect(setFilterValue).toHaveBeenCalledWith('Query'); + }); + + it('renders IndexingStatus (org)', () => { + setMockValues({ ...mockValues, mostRecentIndexJob }); + const wrapper = shallow(); + + expect(wrapper.find(IndexingStatus)).toHaveLength(1); + expect(wrapper.find(IndexingStatus).prop('statusPath')).toEqual( + '/api/workplace_search/org/sources/123/reindex_job/123/status' + ); + }); + + it('renders IndexingStatus (account)', () => { + setMockValues({ ...mockValues, mostRecentIndexJob, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.find(IndexingStatus)).toHaveLength(1); + expect(wrapper.find(IndexingStatus).prop('statusPath')).toEqual( + '/api/workplace_search/account/sources/123/reindex_job/123/status' + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx new file mode 100644 index 0000000000000..35a086369f390 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useParams } from 'react-router-dom'; + +import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; + +import { SchemaChangeErrors } from './schema_change_errors'; + +describe('SchemaChangeErrors', () => { + const fieldCoercionErrors = [] as any; + const serverSchema = { + foo: 'string', + }; + it('renders', () => { + setMockValues({ fieldCoercionErrors, serverSchema }); + setMockActions({ initializeSchemaFieldErrors: jest.fn() }); + + (useParams as jest.Mock).mockImplementationOnce(() => ({ + activeReindexJobId: '1', + sourceId: '123', + })); + const wrapper = shallow(); + + expect(wrapper.find(SchemaErrorsAccordion)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx new file mode 100644 index 0000000000000..cf16a04d92ecf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; + +import { SchemaFieldsTable } from './schema_fields_table'; + +describe('SchemaFieldsTable', () => { + const filterValue = ''; + const filteredSchemaFields = { + foo: 'string', + }; + + beforeEach(() => { + setMockActions({ updateExistingFieldType: jest.fn() }); + }); + + it('renders', () => { + setMockValues({ filterValue, filteredSchemaFields }); + const wrapper = shallow(); + + expect(wrapper.find(SchemaExistingField)).toHaveLength(1); + }); + + it('handles no results', () => { + setMockValues({ filterValue, filteredSchemaFields: {} }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="NoResultsMessage"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index 8f697b2b5c35d..1670e2128c0af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -66,7 +66,7 @@ export const SchemaFieldsTable: React.FC = () => { ) : ( -

+

{i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.filter.noResults.message', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts new file mode 100644 index 0000000000000..2c3aa6114c7da --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -0,0 +1,470 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, + expectedAsyncError, +} from '../../../../../__mocks__'; + +const contentSource = { id: 'source123' }; +jest.mock('../../source_logic', () => ({ + SourceLogic: { values: { contentSource } }, +})); + +import { AppLogic } from '../../../../app_logic'; +jest.mock('../../../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +const spyScrollTo = jest.fn(); +Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); + +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; +import { TEXT } from '../../../../../shared/constants/field_types'; +import { ADD, UPDATE } from '../../../../../shared/constants/operations'; + +import { + SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, + SCHEMA_FIELD_ADDED_MESSAGE, + SCHEMA_UPDATED_MESSAGE, +} from './constants'; + +import { SchemaLogic, dataTypeOptions } from './schema_logic'; + +describe('SchemaLogic', () => { + const { http } = mockHttpValues; + const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(SchemaLogic); + + const defaultValues = { + sourceId: '', + activeSchema: {}, + serverSchema: {}, + filterValue: '', + filteredSchemaFields: {}, + dataTypeOptions, + showAddFieldModal: false, + addFieldFormErrors: null, + mostRecentIndexJob: {}, + fieldCoercionErrors: {}, + newFieldType: TEXT, + rawFieldName: '', + formUnchanged: true, + dataLoading: true, + }; + + const schema = { + foo: 'text', + } as any; + + const fieldCoercionErrors = [ + { + external_id: '123', + error: 'error', + }, + ] as any; + + const errors = ['this is an error']; + + const serverResponse = { + schema, + sourceId: contentSource.id, + mostRecentIndexJob, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(SchemaLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('onInitializeSchema', () => { + SchemaLogic.actions.onInitializeSchema(serverResponse); + + expect(SchemaLogic.values.sourceId).toEqual(contentSource.id); + expect(SchemaLogic.values.activeSchema).toEqual(schema); + expect(SchemaLogic.values.serverSchema).toEqual(schema); + expect(SchemaLogic.values.mostRecentIndexJob).toEqual(mostRecentIndexJob); + expect(SchemaLogic.values.dataLoading).toEqual(false); + }); + + it('onInitializeSchemaFieldErrors', () => { + SchemaLogic.actions.onInitializeSchemaFieldErrors({ fieldCoercionErrors }); + + expect(SchemaLogic.values.fieldCoercionErrors).toEqual(fieldCoercionErrors); + }); + it('onSchemaSetSuccess', () => { + SchemaLogic.actions.onSchemaSetSuccess({ + schema, + mostRecentIndexJob, + }); + + expect(SchemaLogic.values.activeSchema).toEqual(schema); + expect(SchemaLogic.values.serverSchema).toEqual(schema); + expect(SchemaLogic.values.mostRecentIndexJob).toEqual(mostRecentIndexJob); + expect(SchemaLogic.values.newFieldType).toEqual(TEXT); + expect(SchemaLogic.values.addFieldFormErrors).toEqual(null); + expect(SchemaLogic.values.formUnchanged).toEqual(true); + expect(SchemaLogic.values.showAddFieldModal).toEqual(false); + expect(SchemaLogic.values.dataLoading).toEqual(false); + expect(SchemaLogic.values.rawFieldName).toEqual(''); + }); + + it('onSchemaSetFormErrors', () => { + SchemaLogic.actions.onSchemaSetFormErrors(errors); + + expect(SchemaLogic.values.addFieldFormErrors).toEqual(errors); + }); + + it('updateNewFieldType', () => { + const NUMBER = 'number'; + SchemaLogic.actions.updateNewFieldType(NUMBER); + + expect(SchemaLogic.values.newFieldType).toEqual(NUMBER); + }); + + it('onFieldUpdate', () => { + SchemaLogic.actions.onFieldUpdate({ schema, formUnchanged: false }); + + expect(SchemaLogic.values.activeSchema).toEqual(schema); + expect(SchemaLogic.values.formUnchanged).toEqual(false); + }); + + it('onIndexingComplete', () => { + SchemaLogic.actions.onIndexingComplete(1); + + expect(SchemaLogic.values.mostRecentIndexJob).toEqual({ + ...mostRecentIndexJob, + activeReindexJobId: undefined, + percentageComplete: 100, + hasErrors: true, + isActive: false, + }); + }); + + it('resetMostRecentIndexJob', () => { + SchemaLogic.actions.resetMostRecentIndexJob(mostRecentIndexJob); + + expect(SchemaLogic.values.mostRecentIndexJob).toEqual(mostRecentIndexJob); + }); + + it('setFieldName', () => { + const NAME = 'name'; + SchemaLogic.actions.setFieldName(NAME); + + expect(SchemaLogic.values.rawFieldName).toEqual(NAME); + }); + + it('setFilterValue', () => { + const VALUE = 'string'; + SchemaLogic.actions.setFilterValue(VALUE); + + expect(SchemaLogic.values.filterValue).toEqual(VALUE); + }); + + it('openAddFieldModal', () => { + SchemaLogic.actions.openAddFieldModal(); + + expect(SchemaLogic.values.showAddFieldModal).toEqual(true); + }); + + it('closeAddFieldModal', () => { + SchemaLogic.actions.onSchemaSetFormErrors(errors); + SchemaLogic.actions.openAddFieldModal(); + SchemaLogic.actions.closeAddFieldModal(); + + expect(SchemaLogic.values.showAddFieldModal).toEqual(false); + expect(SchemaLogic.values.addFieldFormErrors).toEqual(null); + }); + + it('resetSchemaState', () => { + SchemaLogic.actions.resetSchemaState(); + + expect(SchemaLogic.values.dataLoading).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('listeners', () => { + describe('initializeSchema', () => { + it('calls API and sets values (org)', async () => { + const onInitializeSchemaSpy = jest.spyOn(SchemaLogic.actions, 'onInitializeSchema'); + const promise = Promise.resolve(serverResponse); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchema(); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/schemas' + ); + await promise; + expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onInitializeSchemaSpy = jest.spyOn(SchemaLogic.actions, 'onInitializeSchema'); + const promise = Promise.resolve(serverResponse); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchema(); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/schemas' + ); + await promise; + expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchema(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('initializeSchemaFieldErrors', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const onInitializeSchemaFieldErrorsSpy = jest.spyOn( + SchemaLogic.actions, + 'onInitializeSchemaFieldErrors' + ); + const initPromise = Promise.resolve(serverResponse); + const promise = Promise.resolve({ fieldCoercionErrors }); + http.get.mockReturnValue(initPromise); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchemaFieldErrors( + mostRecentIndexJob.activeReindexJobId, + contentSource.id + ); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/schemas' + ); + + await initPromise; + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/reindex_job/123' + ); + + await promise; + expect(onInitializeSchemaFieldErrorsSpy).toHaveBeenCalledWith({ + fieldCoercionErrors, + }); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onInitializeSchemaFieldErrorsSpy = jest.spyOn( + SchemaLogic.actions, + 'onInitializeSchemaFieldErrors' + ); + const initPromise = Promise.resolve(serverResponse); + const promise = Promise.resolve({ fieldCoercionErrors }); + http.get.mockReturnValue(initPromise); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchemaFieldErrors( + mostRecentIndexJob.activeReindexJobId, + contentSource.id + ); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/schemas' + ); + + await initPromise; + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/reindex_job/123' + ); + + await promise; + expect(onInitializeSchemaFieldErrorsSpy).toHaveBeenCalledWith({ + fieldCoercionErrors, + }); + }); + + it('handles error', async () => { + const promise = Promise.reject({ error: 'this is an error' }); + http.get.mockReturnValue(promise); + SchemaLogic.actions.initializeSchemaFieldErrors( + mostRecentIndexJob.activeReindexJobId, + contentSource.id + ); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + error: 'this is an error', + message: SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, + }); + }); + }); + + it('addNewField', () => { + const setServerFieldSpy = jest.spyOn(SchemaLogic.actions, 'setServerField'); + SchemaLogic.actions.onInitializeSchema(serverResponse); + const newSchema = { + ...schema, + bar: 'number', + }; + SchemaLogic.actions.addNewField('bar', 'number'); + + expect(setServerFieldSpy).toHaveBeenCalledWith(newSchema, ADD); + }); + + it('updateExistingFieldType', () => { + const onFieldUpdateSpy = jest.spyOn(SchemaLogic.actions, 'onFieldUpdate'); + SchemaLogic.actions.onInitializeSchema(serverResponse); + const newSchema = { + foo: 'number', + }; + SchemaLogic.actions.updateExistingFieldType('foo', 'number'); + + expect(onFieldUpdateSpy).toHaveBeenCalledWith({ schema: newSchema, formUnchanged: false }); + }); + + it('updateFields', () => { + const setServerFieldSpy = jest.spyOn(SchemaLogic.actions, 'setServerField'); + SchemaLogic.actions.onInitializeSchema(serverResponse); + SchemaLogic.actions.updateFields(); + + expect(setServerFieldSpy).toHaveBeenCalledWith(schema, UPDATE); + }); + + describe('setServerField', () => { + beforeEach(() => { + SchemaLogic.actions.onInitializeSchema(serverResponse); + }); + + describe('adding a field', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); + const promise = Promise.resolve(serverResponse); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, ADD); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/schemas', + { + body: JSON.stringify({ ...schema }), + } + ); + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_FIELD_ADDED_MESSAGE); + expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); + const promise = Promise.resolve(serverResponse); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, ADD); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/schemas', + { + body: JSON.stringify({ ...schema }), + } + ); + await promise; + expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('handles error', async () => { + const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors'); + const promise = Promise.reject({ message: 'this is an error' }); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, ADD); + await expectedAsyncError(promise); + + expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('updating a field', () => { + it('calls API and sets values (org)', async () => { + AppLogic.values.isOrganization = true; + const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); + const promise = Promise.resolve(serverResponse); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, UPDATE); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/schemas', + { + body: JSON.stringify({ ...schema }), + } + ); + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_UPDATED_MESSAGE); + expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onSchemaSetSuccessSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetSuccess'); + const promise = Promise.resolve(serverResponse); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, UPDATE); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/schemas', + { + body: JSON.stringify({ ...schema }), + } + ); + await promise; + expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.post.mockReturnValue(promise); + SchemaLogic.actions.setServerField(schema, UPDATE); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + }); + }); + + describe('selectors', () => { + describe('filteredSchemaFields', () => { + it('handles empty response', () => { + SchemaLogic.actions.onInitializeSchema(serverResponse); + SchemaLogic.actions.setFilterValue('baz'); + + expect(SchemaLogic.values.filteredSchemaFields).toEqual({}); + }); + + it('handles filtered response', () => { + const newSchema = { + ...schema, + bar: 'number', + }; + SchemaLogic.actions.onInitializeSchema(serverResponse); + SchemaLogic.actions.onFieldUpdate({ schema: newSchema, formUnchanged: false }); + SchemaLogic.actions.setFilterValue('foo'); + + expect(SchemaLogic.values.filteredSchemaFields).toEqual(schema); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index 36eb3fc67b2c2..9a9435a07e245 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -17,7 +17,7 @@ import { OptionValue } from '../../../../types'; import { flashAPIErrors, setSuccessMessage, - FlashMessagesLogic, + clearFlashMessages, } from '../../../../../shared/flash_messages'; import { AppLogic } from '../../../../app_logic'; @@ -46,7 +46,6 @@ interface SchemaActions { }): { schema: Schema; formUnchanged: boolean }; onIndexingComplete(numDocumentsWithErrors: number): number; resetMostRecentIndexJob(emptyReindexJob: IndexJob): IndexJob; - showFieldSuccess(successMessage: string): string; setFieldName(rawFieldName: string): string; setFilterValue(filterValue: string): string; addNewField( @@ -111,7 +110,7 @@ interface SchemaChangeErrorsProps { fieldCoercionErrors: FieldCoercionErrors; } -const dataTypeOptions = [ +export const dataTypeOptions = [ { value: 'text', text: 'Text' }, { value: 'date', text: 'Date' }, { value: 'number', text: 'Number' }, @@ -132,7 +131,6 @@ export const SchemaLogic = kea>({ }), onIndexingComplete: (numDocumentsWithErrors: number) => numDocumentsWithErrors, resetMostRecentIndexJob: (emptyReindexJob: IndexJob) => emptyReindexJob, - showFieldSuccess: (successMessage: string) => successMessage, setFieldName: (rawFieldName: string) => rawFieldName, setFilterValue: (filterValue: string) => filterValue, openAddFieldModal: () => true, @@ -326,7 +324,7 @@ export const SchemaLogic = kea>({ const emptyReindexJob = { percentageComplete: 100, numDocumentsWithErrors: 0, - activeReindexJobId: 0, + activeReindexJobId: '', isActive: false, }; @@ -348,10 +346,10 @@ export const SchemaLogic = kea>({ } }, resetMostRecentIndexJob: () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); }, resetSchemaState: () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 0891687d425f5..9a68d2234e3ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -15,7 +15,7 @@ import { flashAPIErrors, setSuccessMessage, setQueuedSuccessMessage, - FlashMessagesLogic, + clearFlashMessages, } from '../../../shared/flash_messages'; import { DEFAULT_META } from '../../../shared/constants'; @@ -246,7 +246,7 @@ export const SourceLogic = kea>({ } }, removeContentSource: async ({ sourceId, successCallback }) => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); const { isOrganization } = AppLogic.values; const route = isOrganization ? `/api/workplace_search/org/sources/${sourceId}` @@ -292,7 +292,7 @@ export const SourceLogic = kea>({ ); }, resetSourceState: () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index cb8df1d312198..ab71f76484561 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -15,7 +15,7 @@ import { HttpLogic } from '../../../shared/http'; import { flashAPIErrors, setQueuedSuccessMessage, - FlashMessagesLogic, + clearFlashMessages, } from '../../../shared/flash_messages'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; @@ -233,7 +233,7 @@ export const SourcesLogic = kea>( ); }, resetFlashMessages: () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index 7485f986076d7..7e3c14b203e9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -22,7 +22,7 @@ import { EuiText, } from '@elastic/eui'; -import { FlashMessagesLogic } from '../../../shared/flash_messages'; +import { clearFlashMessages } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; @@ -49,7 +49,7 @@ export const SourcesView: React.FC = ({ children }) => { const pollingInterval = window.setInterval(pollForSourceStatusChanges, POLLING_INTERVAL); return () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); clearInterval(pollingInterval); }; }, []); From 97c7f5c8a1744cee31f679d71f4809ba131ff259 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 20 Jan 2021 20:51:18 -0700 Subject: [PATCH 27/28] skip flaky suite (#88926) (#88927) (#88929) --- x-pack/test/accessibility/apps/lens.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index bfd79f070d284..a7cacd0ad1cbb 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -12,7 +12,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - describe('Lens', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88926 + // FLAKY: https://github.com/elastic/kibana/issues/88927 + // FLAKY: https://github.com/elastic/kibana/issues/88929 + describe.skip('Lens', () => { const lensChartName = 'MyLensChart'; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { From d28fa36e8a3047d354723edb3f8e4d7d55272811 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 20 Jan 2021 21:01:57 -0700 Subject: [PATCH 28/28] skip flaky suite (#88928) --- .../search/sessions_mgmt/components/table/table.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 357f17649394b..51cec8f2afeff 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -127,7 +127,8 @@ describe('Background Search Session Management Table', () => { }); }); - describe('fetching sessions data', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88928 + describe.skip('fetching sessions data', () => { test('re-fetches data', async () => { jest.useFakeTimers(); sessionsClient.find = jest.fn();