diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts index 634f2cd8f59cc..4bab5938cf98b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts @@ -6,3 +6,6 @@ */ export const MIN_PAGE_SIZE = 10; + +export const HISTORY_TAB_ID = 'history'; +export const LATEST_CHECK_TAB_ID = 'latest_check'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/index.tsx new file mode 100644 index 0000000000000..2dce7afc55995 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext, useContext } from 'react'; +import { HistoricalResultsValue } from './types'; + +export const HistoricalResultsContext = createContext(null); + +export const useHistoricalResultsContext = () => { + const context = useContext(HistoricalResultsContext); + if (context == null) { + throw new Error( + 'useHistoricalResultsContext must be used inside the HistoricalResultsContextProvider.' + ); + } + return context; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/types.ts new file mode 100644 index 0000000000000..727029797016b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/contexts/historical_results_context/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HistoricalResult } from '../../../../../types'; +import { UseHistoricalResultsFetch } from '../../index_check_flyout/types'; + +export interface HistoricalResultsValue { + historicalResultsState: { + results: HistoricalResult[]; + total: number; + isLoading: boolean; + error: Error | null; + }; + fetchHistoricalResults: UseHistoricalResultsFetch; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/constants.ts similarity index 51% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/constants.ts index 889f014a66f1b..9eae3d5e6c4fc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/constants.ts @@ -5,8 +5,4 @@ * 2.0. */ -export const ALL_TAB_ID = 'allTab'; -export const ECS_COMPLIANT_TAB_ID = 'ecsCompliantTab'; -export const CUSTOM_TAB_ID = 'customTab'; -export const INCOMPATIBLE_TAB_ID = 'incompatibleTab'; -export const SAME_FAMILY_TAB_ID = 'sameFamilyTab'; +export const GET_INDEX_RESULTS = '/internal/ecs_data_quality_dashboard/results/{indexName}'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.test.tsx new file mode 100644 index 0000000000000..36a1a24192e99 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { mockHistoricalResult } from '../../../../../mock/historical_results/mock_historical_results_response'; +import { TestDataQualityProviders } from '../../../../../mock/test_providers/test_providers'; +import * as fetchHistoricalResults from './utils/fetch_historical_results'; +import { useHistoricalResults } from '.'; + +describe('useHistoricalResults', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('should return initial historical results state and fetch historical results function', () => { + const { result } = renderHook(() => useHistoricalResults(), { + wrapper: TestDataQualityProviders, + }); + + expect(result.current.historicalResultsState).toEqual({ + results: [], + total: 0, + isLoading: true, + error: null, + }); + + expect(result.current.fetchHistoricalResults).toBeInstanceOf(Function); + }); + + describe('when fetchHistoricalResults is called', () => { + it('should fetch historical results and update historical results state', async () => { + const fetchResultsSpy = jest + .spyOn(fetchHistoricalResults, 'fetchHistoricalResults') + .mockResolvedValue({ + results: [mockHistoricalResult], + total: 1, + }); + + const { result } = renderHook(() => useHistoricalResults(), { + wrapper: TestDataQualityProviders, + }); + + const abortController = new AbortController(); + + await act(() => + result.current.fetchHistoricalResults({ + abortController, + indexName: 'indexName', + size: 10, + from: 0, + startDate: 'now-7d', + endDate: 'now', + outcome: 'pass', + }) + ); + + expect(fetchResultsSpy).toHaveBeenCalledWith({ + indexName: 'indexName', + httpFetch: expect.any(Function), + abortController, + size: 10, + from: 0, + startDate: 'now-7d', + endDate: 'now', + outcome: 'pass', + }); + + expect(result.current.historicalResultsState).toEqual({ + results: [mockHistoricalResult], + total: 1, + isLoading: false, + error: null, + }); + }); + }); + + describe('when fetchHistoricalResults fails', () => { + it('should update historical results state with error', async () => { + const fetchResultsSpy = jest + .spyOn(fetchHistoricalResults, 'fetchHistoricalResults') + .mockRejectedValue(new Error('An error occurred')); + + const { result } = renderHook(() => useHistoricalResults(), { + wrapper: TestDataQualityProviders, + }); + + const abortController = new AbortController(); + + await act(() => + result.current.fetchHistoricalResults({ + abortController, + indexName: 'indexName', + size: 10, + from: 0, + startDate: 'now-7d', + endDate: 'now', + outcome: 'pass', + }) + ); + + expect(fetchResultsSpy).toHaveBeenCalledWith({ + indexName: 'indexName', + httpFetch: expect.any(Function), + abortController, + size: 10, + from: 0, + startDate: 'now-7d', + endDate: 'now', + outcome: 'pass', + }); + + expect(result.current.historicalResultsState).toEqual({ + results: [], + total: 0, + isLoading: false, + error: new Error('An error occurred'), + }); + }); + }); + + describe('during fetchHistoricalResults call', () => { + it('should set isLoading to true', async () => { + jest.spyOn(fetchHistoricalResults, 'fetchHistoricalResults').mockImplementation(() => { + return new Promise(() => {}); + }); + + const { result } = renderHook(() => useHistoricalResults(), { + wrapper: TestDataQualityProviders, + }); + + const abortController = new AbortController(); + + act(() => { + result.current.fetchHistoricalResults({ + abortController, + indexName: 'indexName', + size: 10, + from: 0, + startDate: 'now-7d', + endDate: 'now', + outcome: 'pass', + }); + }); + + expect(result.current.historicalResultsState).toEqual({ + results: [], + total: 0, + isLoading: true, + error: null, + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.tsx new file mode 100644 index 0000000000000..00b81fc6272c0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useReducer, useCallback } from 'react'; + +import { GET_RESULTS_ERROR_TITLE } from '../../../../../translations'; +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { useIsMountedRef } from '../../../../../hooks/use_is_mounted_ref'; +import { fetchHistoricalResults } from './utils/fetch_historical_results'; +import { FetchHistoricalResultsReducerState, UseHistoricalResultsReturnValue } from './types'; +import { UseHistoricalResultsFetchOpts } from '../../index_check_flyout/types'; +import { fetchHistoricalResultsReducer } from './reducers/fetch_historical_results_reducer'; + +export const initialFetchHistoricalResultsReducerState: FetchHistoricalResultsReducerState = { + results: [], + total: 0, + isLoading: true, + error: null, +}; + +export const useHistoricalResults = (): UseHistoricalResultsReturnValue => { + const [state, dispatch] = useReducer( + fetchHistoricalResultsReducer, + initialFetchHistoricalResultsReducerState + ); + const { httpFetch, toasts } = useDataQualityContext(); + const { isMountedRef } = useIsMountedRef(); + + const fetchResults = useCallback( + async ({ + abortController, + indexName, + size, + from, + startDate, + endDate, + outcome, + }: UseHistoricalResultsFetchOpts) => { + dispatch({ type: 'FETCH_START' }); + + try { + const { results, total } = await fetchHistoricalResults({ + indexName, + httpFetch, + abortController, + size, + from, + startDate, + endDate, + outcome, + }); + + if (isMountedRef.current) { + dispatch({ + type: 'FETCH_SUCCESS', + payload: { + results, + total, + }, + }); + } + } catch (error) { + if (isMountedRef.current) { + toasts.addError(error, { title: GET_RESULTS_ERROR_TITLE }); + dispatch({ type: 'FETCH_ERROR', payload: error }); + } + } + }, + [dispatch, httpFetch, toasts, isMountedRef] + ); + + return { + historicalResultsState: { + results: state.results, + total: state.total, + isLoading: state.isLoading, + error: state.error, + }, + fetchHistoricalResults: fetchResults, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.test.ts new file mode 100644 index 0000000000000..9f83094097cb0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHistoricalResult } from '../../../../../../mock/historical_results/mock_historical_results_response'; +import { fetchHistoricalResultsReducer } from './fetch_historical_results_reducer'; + +const getInitialState = () => ({ + results: [], + total: 0, + isLoading: true, + error: null, +}); + +describe('fetchHistoricalResultsReducer', () => { + describe('on fetch start', () => { + it('should return initial state', () => { + expect(fetchHistoricalResultsReducer(getInitialState(), { type: 'FETCH_START' })).toEqual({ + results: [], + total: 0, + isLoading: true, + error: null, + }); + }); + }); + + describe('on fetch success', () => { + it('should update state with fetched results', () => { + const results = [mockHistoricalResult]; + const total = 1; + + expect( + fetchHistoricalResultsReducer(getInitialState(), { + type: 'FETCH_SUCCESS', + payload: { results, total }, + }) + ).toEqual({ + results, + total, + isLoading: false, + error: null, + }); + }); + }); + + describe('on fetch error', () => { + it('should update state with error', () => { + const error = new Error('An error occurred'); + + expect( + fetchHistoricalResultsReducer(getInitialState(), { + type: 'FETCH_ERROR', + payload: error, + }) + ).toEqual({ + results: [], + total: 0, + isLoading: false, + error, + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.ts new file mode 100644 index 0000000000000..4d009b69e35e7 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/reducers/fetch_historical_results_reducer.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HistoricalResult } from '../../../../../../types'; +import { FetchHistoricalResultsReducerState } from '../types'; + +type Action = + | { type: 'FETCH_SUCCESS'; payload: { results: HistoricalResult[]; total: number } } + | { type: 'FETCH_START' } + | { type: 'FETCH_ERROR'; payload: Error }; + +export const fetchHistoricalResultsReducer = ( + state: FetchHistoricalResultsReducerState, + action: Action +) => { + switch (action.type) { + case 'FETCH_SUCCESS': + return { + results: action.payload.results, + total: action.payload.total, + isLoading: false, + error: null, + }; + case 'FETCH_START': + return { + results: [], + total: 0, + isLoading: true, + error: null, + }; + case 'FETCH_ERROR': + return { + results: [], + total: 0, + isLoading: false, + error: action.payload, + }; + default: + return state; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/types.ts new file mode 100644 index 0000000000000..4fabce71a8087 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HistoricalResult } from '../../../../../types'; +import { UseHistoricalResultsFetch } from '../../index_check_flyout/types'; + +export interface UseHistoricalResultsReturnValue { + historicalResultsState: FetchHistoricalResultsReducerState; + fetchHistoricalResults: UseHistoricalResultsFetch; +} + +export interface FetchHistoricalResultsReducerState { + results: HistoricalResult[]; + total: number; + isLoading: boolean; + error: Error | null; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.test.ts new file mode 100644 index 0000000000000..e0d3b9a9cb4fa --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INTERNAL_API_VERSION } from '../../../../../../constants'; +import { + DEFAULT_HISTORICAL_RESULTS_END_DATE, + DEFAULT_HISTORICAL_RESULTS_START_DATE, +} from '../../../index_check_flyout/constants'; +import { fetchHistoricalResults } from './fetch_historical_results'; + +const indexName = 'test-index'; + +const path = `/internal/ecs_data_quality_dashboard/results/${indexName}`; +const opts = { + method: 'GET', + query: { + endDate: DEFAULT_HISTORICAL_RESULTS_END_DATE, + startDate: DEFAULT_HISTORICAL_RESULTS_START_DATE, + }, + version: INTERNAL_API_VERSION, +}; + +describe('fetchHistoricalResults', () => { + it('should call historical results api for given indexName, internal api version with last week query params and abortcontroller signal', async () => { + const httpFetch = jest.fn().mockResolvedValue({ data: [], total: 0 }); + const abortController = new AbortController(); + await fetchHistoricalResults({ + indexName, + httpFetch, + abortController, + }); + expect(httpFetch).toHaveBeenCalledWith(path, { + ...opts, + signal: abortController.signal, + }); + }); + + it('should return with historical results and total', async () => { + const httpFetch = jest.fn().mockResolvedValue({ data: [], total: 0 }); + await expect( + fetchHistoricalResults({ + indexName, + httpFetch, + abortController: new AbortController(), + }) + ).resolves.toEqual({ results: [], total: 0 }); + }); + + describe('given additional query params', () => { + it('should include them in the query', async () => { + const httpFetch = jest.fn().mockResolvedValue({ data: [], total: 0 }); + const abortController = new AbortController(); + await fetchHistoricalResults({ + indexName: 'test-index', + httpFetch, + abortController: new AbortController(), + startDate: 'now-2d/d', + endDate: 'now-1d/d', + size: 10, + from: 0, + outcome: 'pass', + }); + + expect(httpFetch).toHaveBeenCalledWith(path, { + ...opts, + query: { + ...opts.query, + startDate: 'now-2d/d', + endDate: 'now-1d/d', + size: 10, + from: 0, + outcome: 'pass', + }, + signal: abortController.signal, + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.ts new file mode 100644 index 0000000000000..fdd5100ad8490 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_historical_results/utils/fetch_historical_results.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpFetchQuery } from '@kbn/core-http-browser'; + +import { HistoricalResult } from '../../../../../../types'; +import { INTERNAL_API_VERSION } from '../../../../../../constants'; +import { GET_INDEX_RESULTS } from '../constants'; +import { + DEFAULT_HISTORICAL_RESULTS_START_DATE, + DEFAULT_HISTORICAL_RESULTS_END_DATE, +} from '../../../index_check_flyout/constants'; +import { FetchHistoricalResultsOpts } from '../../../index_check_flyout/types'; + +export interface FetchHistoricalResultsResponse { + data: HistoricalResult[]; + total: number; +} + +export interface FetchHistoricalResultsReturnValue { + results: HistoricalResult[]; + total: number; +} + +export async function fetchHistoricalResults({ + indexName, + size, + from, + startDate, + endDate, + outcome, + httpFetch, + abortController, +}: FetchHistoricalResultsOpts): Promise { + const query: HttpFetchQuery = { + startDate: DEFAULT_HISTORICAL_RESULTS_START_DATE, + endDate: DEFAULT_HISTORICAL_RESULTS_END_DATE, + }; + + if (from !== undefined) { + query.from = from; + } + + if (size !== undefined) { + query.size = size; + } + + if (outcome !== undefined) { + query.outcome = outcome; + } + + if (startDate !== undefined) { + query.startDate = startDate; + } + + if (endDate !== undefined) { + query.endDate = endDate; + } + + const route = GET_INDEX_RESULTS.replace('{indexName}', indexName); + const results = await httpFetch(route, { + method: 'GET', + signal: abortController.signal, + version: INTERNAL_API_VERSION, + query, + }); + return { + results: results.data, + total: results.total, + }; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_ilm_explain/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_ilm_explain/index.tsx index 17d8b8d46dea7..d768f28973770 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_ilm_explain/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_ilm_explain/index.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from 'react'; import { useDataQualityContext } from '../../../../../data_quality_context'; import { INTERNAL_API_VERSION } from '../../../../../constants'; import * as i18n from '../../../../../translations'; -import { useIsMounted } from '../../../../../hooks/use_is_mounted'; +import { useIsMountedRef } from '../../../../../hooks/use_is_mounted_ref'; const ILM_EXPLAIN_ENDPOINT = '/internal/ecs_data_quality_dashboard/ilm_explain'; @@ -23,7 +23,7 @@ export interface UseIlmExplain { export const useIlmExplain = (pattern: string): UseIlmExplain => { const { httpFetch, isILMAvailable } = useDataQualityContext(); - const { isMountedRef } = useIsMounted(); + const { isMountedRef } = useIsMountedRef(); const [ilmExplain, setIlmExplain] = useState { - const { isMountedRef } = useIsMounted(); + const { isMountedRef } = useIsMountedRef(); const { httpFetch, isILMAvailable } = useDataQualityContext(); const [stats, setStats] = useState | null>(null); const [error, setError] = useState(null); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx index cbed456f9cdd5..a165378df80ed 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx @@ -5,28 +5,25 @@ * 2.0. */ -import numeral from '@elastic/numeral'; -import { render, screen } from '@testing-library/react'; -import React, { ComponentProps } from 'react'; +import React from 'react'; +import { act, render, screen, within } from '@testing-library/react'; -import { EMPTY_STAT } from '../../../constants'; import { TestDataQualityProviders, TestExternalProviders, } from '../../../mock/test_providers/test_providers'; import { Pattern } from '.'; -import { getCheckState } from '../../../stub/get_check_state'; +import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { useIlmExplain } from './hooks/use_ilm_explain'; +import { useStats } from './hooks/use_stats'; +import { ERROR_LOADING_METADATA_TITLE, LOADING_STATS } from './translations'; +import { useHistoricalResults } from './hooks/use_historical_results'; +import { getHistoricalResultStub } from '../../../stub/get_historical_result_stub'; -const indexName = 'auditbeat-custom-index-1'; -const defaultBytesFormat = '0,0.[0]b'; -const formatBytes = (value: number | undefined) => - value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; - -const defaultNumberFormat = '0,0.[000]'; -const formatNumber = (value: number | undefined) => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; +const pattern = 'auditbeat-*'; jest.mock('./hooks/use_stats', () => ({ + ...jest.requireActual('./hooks/use_stats'), useStats: jest.fn(() => ({ stats: {}, error: null, @@ -35,6 +32,7 @@ jest.mock('./hooks/use_stats', () => ({ })); jest.mock('./hooks/use_ilm_explain', () => ({ + ...jest.requireActual('./hooks/use_ilm_explain'), useIlmExplain: jest.fn(() => ({ error: null, ilmExplain: {}, @@ -42,64 +40,497 @@ jest.mock('./hooks/use_ilm_explain', () => ({ })), })); -const ilmPhases = ['hot', 'warm', 'unmanaged']; - -const defaultProps: ComponentProps = { - pattern: '', - patternRollup: undefined, - chartSelectedIndex: null, - setChartSelectedIndex: jest.fn(), - indexNames: undefined, -}; +jest.mock('./hooks/use_historical_results', () => ({ + ...jest.requireActual('./hooks/use_historical_results'), + useHistoricalResults: jest.fn(() => ({ + historicalResultsState: { + results: [], + total: 0, + isLoading: true, + error: null, + }, + fetchHistoricalResults: jest.fn(), + })), +})); describe('pattern', () => { beforeEach(() => { jest.clearAllMocks(); + jest.restoreAllMocks(); }); - test('it renders the remote clusters callout when the pattern includes a colon', () => { - const pattern = 'remote:*'; // <-- a colon in the pattern indicates the use of cross cluster search + it('renders the initially open accordion with the pattern data and summary table', () => { + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: auditbeatWithAllResults.ilmExplain, + loading: false, + }); + + (useStats as jest.Mock).mockReturnValue({ + stats: auditbeatWithAllResults.stats, + error: null, + loading: false, + }); render( - - + + ); - expect(screen.getByTestId('remoteClustersCallout')).toBeInTheDocument(); + const accordionToggle = screen.getByRole('button', { + name: 'Fail auditbeat-* hot (1) unmanaged (2) Incompatible fields 4 Indices checked 3 Indices 3 Size 17.9MB Docs 19,127', + }); + + expect(accordionToggle).toBeInTheDocument(); + expect(accordionToggle).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByTestId('summaryTable')).toBeInTheDocument(); }); - test('it does NOT render the remote clusters callout when the pattern does NOT include a colon', () => { - const pattern = 'auditbeat-*'; // <-- no colon in the pattern + describe('remote clusters callout', () => { + describe('when the pattern includes a colon', () => { + it('it renders the remote clusters callout', () => { + render( + + + + + + ); - render( - - - - - - ); + expect(screen.getByTestId('remoteClustersCallout')).toBeInTheDocument(); + }); + }); + + describe('when the pattern does NOT include a colon', () => { + it('it does NOT render the remote clusters callout', () => { + render( + + + + + + ); + + expect(screen.queryByTestId('remoteClustersCallout')).not.toBeInTheDocument(); + }); + }); + }); + + describe('loading & error', () => { + describe('when useStats returns error', () => { + it('renders the error message', () => { + (useStats as jest.Mock).mockReturnValue({ + stats: {}, + error: 'An error occurred', + loading: false, + }); + + render( + + + + + + ); + + expect(screen.getByText(ERROR_LOADING_METADATA_TITLE(pattern))).toBeInTheDocument(); + expect(screen.queryByTestId('summaryTable')).not.toBeInTheDocument(); + }); + }); + + describe('when useIlmExplain returns error', () => { + it('renders the error message', () => { + (useIlmExplain as jest.Mock).mockReturnValue({ + error: 'An error occurred', + ilmExplain: {}, + loading: false, + }); + + render( + + + + + + ); + + expect(screen.getByText(ERROR_LOADING_METADATA_TITLE(pattern))).toBeInTheDocument(); + expect(screen.queryByTestId('summaryTable')).not.toBeInTheDocument(); + }); + }); + + describe('when useStats is loading but useIlmExplan returns error', () => { + it('renders the loading message', () => { + (useStats as jest.Mock).mockReturnValue({ + stats: {}, + error: null, + loading: true, + }); + + (useIlmExplain as jest.Mock).mockReturnValue({ + error: 'An error occurred', + ilmExplain: {}, + loading: false, + }); + + render( + + + + + + ); + + expect(screen.getByText(LOADING_STATS)).toBeInTheDocument(); + expect(screen.queryByTestId('summaryTable')).not.toBeInTheDocument(); + }); + }); + + describe('when useIlmExplain is loading but useStats returns error', () => { + it('renders the loading message', () => { + (useStats as jest.Mock).mockReturnValue({ + stats: {}, + error: 'An error occurred', + loading: false, + }); + + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: {}, + loading: true, + }); + + render( + + + + + + ); + + expect(screen.getByText(LOADING_STATS)).toBeInTheDocument(); + expect(screen.queryByTestId('summaryTable')).not.toBeInTheDocument(); + }); + }); + }); + + describe('Flyout', () => { + describe('when the check now action is clicked', () => { + it('calls the checkIndex function and opens flyout with latest check tab', async () => { + const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; + // arrange + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: auditbeatWithAllResults.ilmExplain, + loading: false, + }); + + (useStats as jest.Mock).mockReturnValue({ + stats: auditbeatWithAllResults.stats, + error: null, + loading: false, + }); + + const checkIndex = jest.fn(); + + // act + render( + + + + + + ); + + const rows = screen.getAllByRole('row'); + const firstBodyRow = within(rows[1]); + + expect(screen.queryByTestId('indexCheckFlyout')).not.toBeInTheDocument(); + + const checkNowButton = firstBodyRow.getByRole('button', { + name: 'Check now', + }); + + await act(async () => checkNowButton.click()); + + // assert + expect(checkIndex).toHaveBeenCalledTimes(1); + expect(checkIndex).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + formatBytes: expect.any(Function), + formatNumber: expect.any(Function), + httpFetch: expect.any(Function), + indexName, + pattern, + }); + expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute( + 'aria-selected', + 'false' + ); + expect(screen.getByTestId('latestResults')).toBeInTheDocument(); + expect(screen.queryByTestId('historicalResults')).not.toBeInTheDocument(); + }); + }); + + describe('when the view history action is clicked', () => { + it('calls the fetchHistoricalResults function and opens flyout with history check tab', async () => { + const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; + // arrange + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: auditbeatWithAllResults.ilmExplain, + loading: false, + }); + + (useStats as jest.Mock).mockReturnValue({ + stats: auditbeatWithAllResults.stats, + error: null, + loading: false, + }); + + const fetchHistoricalResults = jest.fn(); + + (useHistoricalResults as jest.Mock).mockReturnValue({ + historicalResultsState: { + results: [getHistoricalResultStub(indexName)], + total: 1, + isLoading: false, + error: null, + }, + fetchHistoricalResults, + }); + + // act + render( + + + + + + ); + + const rows = screen.getAllByRole('row'); + const firstBodyRow = within(rows[1]); + + expect(screen.queryByTestId('indexCheckFlyout')).not.toBeInTheDocument(); + + const viewHistoryButton = firstBodyRow.getByRole('button', { + name: 'View history', + }); + + await act(async () => viewHistoryButton.click()); + + // assert + expect(fetchHistoricalResults).toHaveBeenCalledTimes(1); + expect(fetchHistoricalResults).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + indexName, + }); + expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute( + 'aria-selected', + 'false' + ); + expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect(screen.queryByTestId('latestResults')).not.toBeInTheDocument(); + expect(screen.getByTestId('historicalResults')).toBeInTheDocument(); + }); + }); + + describe('when the close button is clicked', () => { + it('closes the flyout', async () => { + const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; + // arrange + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: auditbeatWithAllResults.ilmExplain, + loading: false, + }); + + (useStats as jest.Mock).mockReturnValue({ + stats: auditbeatWithAllResults.stats, + error: null, + loading: false, + }); + + const fetchHistoricalResults = jest.fn(); + + (useHistoricalResults as jest.Mock).mockReturnValue({ + historicalResultsState: { + results: [getHistoricalResultStub(indexName)], + total: 1, + isLoading: false, + error: null, + }, + fetchHistoricalResults, + }); + + // act + render( + + + + + + ); + + const rows = screen.getAllByRole('row'); + const firstBodyRow = within(rows[1]); + + expect(screen.queryByTestId('indexCheckFlyout')).not.toBeInTheDocument(); + + const viewHistoryButton = firstBodyRow.getByRole('button', { + name: 'View history', + }); + + await act(async () => viewHistoryButton.click()); + + const closeButton = screen.getByRole('button', { name: 'Close this dialog' }); + + await act(async () => closeButton.click()); + + // assert + expect(screen.queryByTestId('indexCheckFlyout')).not.toBeInTheDocument(); + }, 15000); + }); + + describe('when chartSelectedIndex is set', () => { + it('invokes the checkIndex function with the selected index and opens flyout', async () => { + const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; + // arrange + (useIlmExplain as jest.Mock).mockReturnValue({ + error: null, + ilmExplain: auditbeatWithAllResults.ilmExplain, + loading: false, + }); + + (useStats as jest.Mock).mockReturnValue({ + stats: auditbeatWithAllResults.stats, + error: null, + loading: false, + }); + + const checkIndex = jest.fn(); + + // act + render( + + + + + + ); - expect(screen.queryByTestId('remoteClustersCallout')).not.toBeInTheDocument(); + // assert + expect(checkIndex).toHaveBeenCalledTimes(1); + expect(checkIndex).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + formatBytes: expect.any(Function), + formatNumber: expect.any(Function), + httpFetch: expect.any(Function), + indexName, + pattern, + }); + expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute( + 'aria-selected', + 'false' + ); + expect(screen.getByTestId('latestResults')).toBeInTheDocument(); + expect(screen.queryByTestId('historicalResults')).not.toBeInTheDocument(); + }); + }); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx index b5688f9c98a33..30c4aa8755a9c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx @@ -27,11 +27,14 @@ import { useResultsRollupContext } from '../../../contexts/results_rollup_contex import { useIndicesCheckContext } from '../../../contexts/indices_check_context'; import { getSummaryTableItems } from '../../../utils/get_summary_table_items'; import { defaultSort } from '../../../constants'; -import { MIN_PAGE_SIZE } from './constants'; +import { HISTORY_TAB_ID, LATEST_CHECK_TAB_ID, MIN_PAGE_SIZE } from './constants'; import { getIlmExplainPhaseCounts } from './utils/ilm_explain'; import { shouldCreateIndexNames } from './utils/should_create_index_names'; import { shouldCreatePatternRollup } from './utils/should_create_pattern_rollup'; import { getPageIndex } from './utils/get_page_index'; +import { useAbortControllerRef } from '../../../hooks/use_abort_controller_ref'; +import { useHistoricalResults } from './hooks/use_historical_results'; +import { HistoricalResultsContext } from './contexts/historical_results_context'; const EMPTY_INDEX_NAMES: string[] = []; @@ -50,9 +53,17 @@ const PatternComponent: React.FC = ({ chartSelectedIndex, setChartSelectedIndex, }) => { + const { historicalResultsState, fetchHistoricalResults } = useHistoricalResults(); + const historicalResultsContextValue = useMemo( + () => ({ + fetchHistoricalResults, + historicalResultsState, + }), + [fetchHistoricalResults, historicalResultsState] + ); const { httpFetch, isILMAvailable, ilmPhases, startDate, endDate, formatBytes, formatNumber } = useDataQualityContext(); - const { checkIndex, checkState } = useIndicesCheckContext(); + const { checkIndex } = useIndicesCheckContext(); const { updatePatternIndexNames, updatePatternRollup } = useResultsRollupContext(); const containerRef = useRef(null); const [sorting, setSorting] = useState(defaultSort); @@ -60,9 +71,12 @@ const PatternComponent: React.FC = ({ const [pageSize, setPageSize] = useState(MIN_PAGE_SIZE); const patternComponentAccordionId = useGeneratedHtmlId({ prefix: 'patternComponentAccordion' }); const [expandedIndexName, setExpandedIndexName] = useState(null); - const flyoutIndexExpandActionAbortControllerRef = useRef(new AbortController()); - const tableRowIndexCheckNowActionAbortControllerRef = useRef(new AbortController()); - const flyoutIndexChartSelectedActionAbortControllerRef = useRef(new AbortController()); + const [initialFlyoutTabId, setInitialFlyoutTabId] = useState< + typeof LATEST_CHECK_TAB_ID | typeof HISTORY_TAB_ID + >(LATEST_CHECK_TAB_ID); + const flyoutCheckNowAndExpandAbortControllerRef = useAbortControllerRef(); + const flyoutViewCheckHistoryAbortControllerRef = useAbortControllerRef(); + const flyoutChartSelectedActionAbortControllerRef = useAbortControllerRef(); const { error: statsError, @@ -114,10 +128,10 @@ const PatternComponent: React.FC = ({ setExpandedIndexName(null); }, []); - const handleFlyoutIndexExpandAction = useCallback( + const handleFlyoutCheckNowAndExpandAction = useCallback( (indexName: string) => { checkIndex({ - abortController: flyoutIndexExpandActionAbortControllerRef.current, + abortController: flyoutCheckNowAndExpandAbortControllerRef.current, indexName, pattern, httpFetch, @@ -125,22 +139,28 @@ const PatternComponent: React.FC = ({ formatNumber, }); setExpandedIndexName(indexName); + setInitialFlyoutTabId(LATEST_CHECK_TAB_ID); }, - [checkIndex, formatBytes, formatNumber, httpFetch, pattern] + [ + checkIndex, + flyoutCheckNowAndExpandAbortControllerRef, + formatBytes, + formatNumber, + httpFetch, + pattern, + ] ); - const handleTableRowIndexCheckNowAction = useCallback( + const handleFlyoutViewCheckHistoryAction = useCallback( (indexName: string) => { - checkIndex({ - abortController: tableRowIndexCheckNowActionAbortControllerRef.current, + fetchHistoricalResults({ + abortController: flyoutViewCheckHistoryAbortControllerRef.current, indexName, - pattern, - httpFetch, - formatBytes, - formatNumber, }); + setExpandedIndexName(indexName); + setInitialFlyoutTabId(HISTORY_TAB_ID); }, - [checkIndex, formatBytes, formatNumber, httpFetch, pattern] + [fetchHistoricalResults, flyoutViewCheckHistoryAbortControllerRef] ); useEffect(() => { @@ -217,7 +237,7 @@ const PatternComponent: React.FC = ({ if (chartSelectedIndex.indexName !== expandedIndexName && !isFlyoutVisible) { checkIndex({ - abortController: flyoutIndexChartSelectedActionAbortControllerRef.current, + abortController: flyoutChartSelectedActionAbortControllerRef.current, indexName: chartSelectedIndex.indexName, pattern: chartSelectedIndex.pattern, httpFetch, @@ -242,92 +262,81 @@ const PatternComponent: React.FC = ({ httpFetch, formatBytes, formatNumber, + flyoutChartSelectedActionAbortControllerRef, ]); - useEffect(() => { - const flyoutIndexExpandActionAbortController = - flyoutIndexExpandActionAbortControllerRef.current; - const tableRowIndexCheckNowActionAbortController = - tableRowIndexCheckNowActionAbortControllerRef.current; - const flyoutIndexChartSelectedActionAbortController = - flyoutIndexChartSelectedActionAbortControllerRef.current; - return () => { - flyoutIndexExpandActionAbortController.abort(); - tableRowIndexCheckNowActionAbortController.abort(); - flyoutIndexChartSelectedActionAbortController.abort(); - }; - }, []); - return (
- - } - > - - {!loading && pattern.includes(':') && ( - <> - - - - )} + + + } + > + + {!loading && pattern.includes(':') && ( + <> + + + + )} - {!loading && error != null && ( - <> - - - - )} + {!loading && error != null && ( + <> + + + + )} - {loading && ( - <> - - - - )} + {loading && ( + <> + + + + )} - {!loading && error == null && ( -
- -
- )} -
-
- {isFlyoutVisible ? ( - - ) : null} + {!loading && error == null && ( +
+ +
+ )} +
+
+ {isFlyoutVisible ? ( + + ) : null} +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.test.tsx new file mode 100644 index 0000000000000..4a977321955f7 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.test.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render, act } from '@testing-library/react'; +import { EuiButtonGroup } from '@elastic/eui'; + +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; +import { + INCOMPATIBLE_TAB_ID, + SAME_FAMILY_TAB_ID, + ALL_TAB_ID, + CUSTOM_TAB_ID, + ECS_COMPLIANT_TAB_ID, +} from '../constants'; +import { CheckFieldsTabs } from '.'; +import { SAME_FAMILY } from '../../../../../translations'; +import userEvent from '@testing-library/user-event'; + +describe('HistoricalCheckFieldsTabs', () => { + it('should render first tab from tabs', () => { + render( + + {'Incompatible content'}, + }, + { + id: SAME_FAMILY_TAB_ID, + name: 'Same family', + badgeColor: 'primary', + badgeCount: 2, + content:
{'Same family content'}
, + }, + { + id: CUSTOM_TAB_ID, + name: 'Custom', + badgeColor: 'primary', + badgeCount: 2, + content:
{'Other content'}
, + }, + { + id: ECS_COMPLIANT_TAB_ID, + name: 'ECS compliant', + badgeColor: 'primary', + badgeCount: 2, + content:
{'ECS compliant content'}
, + }, + { + id: ALL_TAB_ID, + name: 'All', + badgeColor: 'primary', + badgeCount: 2, + content:
{'All content'}
, + }, + ]} + renderButtonGroup={(props) => } + /> +
+ ); + + expect(screen.getByTestId(INCOMPATIBLE_TAB_ID)).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId(SAME_FAMILY_TAB_ID)).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId(CUSTOM_TAB_ID)).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId(ECS_COMPLIANT_TAB_ID)).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId(ALL_TAB_ID)).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId('tested1')).toBeInTheDocument(); + expect(screen.queryByTestId('tested2')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tested3')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tested4')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tested5')).not.toBeInTheDocument(); + }); + + describe.each([ + [SAME_FAMILY_TAB_ID, 'Same family', 'tested2'], + [CUSTOM_TAB_ID, 'Custom', 'tested3'], + [ECS_COMPLIANT_TAB_ID, 'ECS compliant', 'tested4'], + [ALL_TAB_ID, 'All', 'tested5'], + ])('when %s is selected', (tabId, tabName, contentTestId) => { + it(`should render selected (${tabName}) tab`, async () => { + render( + + {'Incompatible content'}, + }, + { + id: SAME_FAMILY_TAB_ID, + name: SAME_FAMILY, + badgeColor: 'primary', + badgeCount: 2, + content:
{'Same family content'}
, + }, + { + id: CUSTOM_TAB_ID, + name: 'Custom', + badgeColor: 'primary', + badgeCount: 2, + content:
{'Other content'}
, + }, + { + id: ECS_COMPLIANT_TAB_ID, + name: 'ECS compliant', + badgeColor: 'primary', + badgeCount: 2, + content:
{'ECS compliant content'}
, + }, + { + id: ALL_TAB_ID, + name: 'All', + badgeColor: 'primary', + badgeCount: 2, + content:
{'All content'}
, + }, + ]} + renderButtonGroup={(props) => } + /> +
+ ); + + await act(async () => userEvent.click(screen.getByTestId(tabId))); + + expect(screen.getByTestId(contentTestId)).toBeInTheDocument(); + }); + }); + + describe('given disabled tab with disabled reason', () => { + it('should render disabled tab', () => { + render( + + {'Incompatible content'}, + }, + { + id: SAME_FAMILY_TAB_ID, + name: SAME_FAMILY, + badgeColor: 'primary', + badgeCount: 2, + content:
{'Same family content'}
, + }, + { + id: CUSTOM_TAB_ID, + name: 'Custom', + badgeColor: 'primary', + badgeCount: 2, + content:
{'Other content'}
, + }, + { + id: ECS_COMPLIANT_TAB_ID, + name: 'ECS compliant', + badgeColor: 'primary', + badgeCount: 2, + content:
{'ECS compliant content'}
, + }, + { + id: ALL_TAB_ID, + name: 'All', + badgeColor: 'primary', + badgeCount: 2, + disabled: true, + disabledReason: 'Disabled reason', + content:
{'All content'}
, + }, + ]} + renderButtonGroup={(props) => } + /> +
+ ); + + expect(screen.getByTestId(ALL_TAB_ID)).toBeDisabled(); + expect(screen.getByTestId('disabledReasonTooltip')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.tsx new file mode 100644 index 0000000000000..b492d98aa7b7a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/index.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { + EuiBadge, + EuiButtonGroup, + EuiButtonGroupProps, + EuiFlexGroup, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; +import styled, { StyledComponent } from 'styled-components'; + +import { CheckFieldsTab, CheckFieldsTabId } from './types'; + +const StyledTabFlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const StyledTabFlexItem = styled.div` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +const StyledBadge = styled(EuiBadge)` + text-align: right; + cursor: pointer; +`; + +interface CheckFieldsSingleButtonGroupProps { + onChange: (id: string) => void; + idSelected: CheckFieldsTabId; + options: EuiButtonGroupProps['options']; + legend: 'Check fields tab group'; + buttonSize: 'compressed'; + color: 'primary'; +} + +export interface Props { + tabs: CheckFieldsTab[]; + renderButtonGroup: ( + props: CheckFieldsSingleButtonGroupProps + ) => React.ReactElement< + EuiButtonGroupProps, + StyledComponent | typeof EuiButtonGroup + >; +} + +const CheckFieldsTabsComponent: React.FC = ({ tabs, renderButtonGroup }) => { + const checkFieldsTabs = useMemo( + () => + tabs.map((tab) => ({ + id: tab.id, + name: tab.name, + append: {tab.badgeCount ?? 0}, + content: tab.content ?? null, + disabled: Boolean(tab.disabled), + ...(tab.disabled && { disabledReason: tab.disabledReason }), + })), + [tabs] + ); + + const [selectedTabId, setSelectedTabId] = useState(() => checkFieldsTabs[0].id); + + const tabSelections = useMemo( + () => + checkFieldsTabs.map((tab) => { + let label = ( + + {tab.name} + {tab.append} + + ); + + if (tab.disabled && tab.disabledReason) { + label = ( + + {label} + + ); + } + + return { + id: tab.id, + label, + textProps: false as false, + disabled: tab.disabled, + }; + }), + [checkFieldsTabs] + ); + + const handleSelectedTabId = useCallback((optionId: string) => { + setSelectedTabId(optionId as CheckFieldsTabId); + }, []); + + return ( +
+ {renderButtonGroup({ + legend: 'Check fields tab group', + options: tabSelections, + idSelected: selectedTabId, + onChange: handleSelectedTabId, + buttonSize: 'compressed', + color: 'primary', + })} + + {checkFieldsTabs.find((tab) => tab.id === selectedTabId)?.content} +
+ ); +}; + +CheckFieldsTabsComponent.displayName = 'CheckFieldsComponent'; + +export const CheckFieldsTabs = React.memo(CheckFieldsTabsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/types.ts new file mode 100644 index 0000000000000..9cb69a9c4a9d2 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_fields_tabs/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + INCOMPATIBLE_TAB_ID, + SAME_FAMILY_TAB_ID, + ALL_TAB_ID, + CUSTOM_TAB_ID, + ECS_COMPLIANT_TAB_ID, +} from '../constants'; + +export interface CheckFieldsTabBase { + name: string; + badgeCount?: number; + badgeColor?: string; + content?: React.ReactNode; + disabled?: boolean; + disabledReason?: string; +} + +export type CheckFieldsTabId = + | typeof INCOMPATIBLE_TAB_ID + | typeof SAME_FAMILY_TAB_ID + | typeof CUSTOM_TAB_ID + | typeof ECS_COMPLIANT_TAB_ID + | typeof ALL_TAB_ID; + +export type CheckFieldsIncompatibleTab = CheckFieldsTabBase & { + id: typeof INCOMPATIBLE_TAB_ID; +}; + +export type CheckFieldsSameFamilyTab = CheckFieldsTabBase & { + id: typeof SAME_FAMILY_TAB_ID; +}; + +export type CheckFieldsCustomTab = CheckFieldsTabBase & { + id: typeof CUSTOM_TAB_ID; +}; + +export type CheckFieldsEcsCompliantTab = CheckFieldsTabBase & { + id: typeof ECS_COMPLIANT_TAB_ID; +}; + +export type CheckFieldsAllTab = CheckFieldsTabBase & { + id: typeof ALL_TAB_ID; +}; + +export type CheckFieldsTab = + | CheckFieldsIncompatibleTab + | CheckFieldsSameFamilyTab + | CheckFieldsCustomTab + | CheckFieldsEcsCompliantTab + | CheckFieldsAllTab; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.test.tsx new file mode 100644 index 0000000000000..d8ec43e6ab6d8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; + +import { INCOMPATIBLE_EMPTY, INCOMPATIBLE_EMPTY_TITLE } from './translations'; +import { CheckSuccessEmptyPrompt } from '.'; + +describe('CheckSuccessEmptyPrompt', () => { + it('should render incompatible empty prompt message', () => { + render(); + + expect(screen.getByText(INCOMPATIBLE_EMPTY_TITLE)).toBeInTheDocument(); + expect(screen.getByText(INCOMPATIBLE_EMPTY)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.tsx new file mode 100644 index 0000000000000..482558864135d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, memo } from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { EmptyPromptBody } from '../empty_prompt_body'; +import { EmptyPromptTitle } from '../empty_prompt_title'; +import { INCOMPATIBLE_EMPTY, INCOMPATIBLE_EMPTY_TITLE } from './translations'; + +const CheckSuccessEmptyPromptComponent = () => { + const body = useMemo(() => , []); + const title = useMemo(() => , []); + + return ( + + ); +}; + +CheckSuccessEmptyPromptComponent.displayName = 'CheckSuccessEmptyPromptComponent'; + +export const CheckSuccessEmptyPrompt = memo(CheckSuccessEmptyPromptComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/translations.ts new file mode 100644 index 0000000000000..2ce04473bc882 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/check_success_empty_prompt/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INCOMPATIBLE_EMPTY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleEmptyContent', + { + defaultMessage: + 'All of the field mappings and document values in this index are compliant with the Elastic Common Schema (ECS).', + } +); + +export const INCOMPATIBLE_EMPTY_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleEmptyTitle', + { + defaultMessage: 'All field mappings and values are ECS compliant', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.test.tsx similarity index 68% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.test.tsx index e7344dad4d55e..adf1235f6cc6c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.test.tsx @@ -8,11 +8,11 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../incompatible_tab/translations'; -import { eventCategory } from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; +import { hostNameWithTextMapping } from '../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; import { CompareFieldsTable } from '.'; -import { getIncompatibleMappingsTableColumns } from './get_incompatible_mappings_table_columns'; +import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../../../../../translations'; +import { getIncompatibleMappingsTableColumns } from '../incompatible_tab/utils/get_incompatible_table_columns'; describe('CompareFieldsTable', () => { describe('rendering', () => { @@ -20,7 +20,7 @@ describe('CompareFieldsTable', () => { render( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.tsx similarity index 91% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.tsx index 8874bd8594866..60d97ed92aab5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/compare_fields_table/index.tsx @@ -9,13 +9,13 @@ import type { EuiTableFieldDataColumnType, Search } from '@elastic/eui'; import { EuiInMemoryTable, EuiTitle, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import * as i18n from './translations'; -import type { EnrichedFieldMetadata } from '../../../../../../../../types'; +import type { EnrichedFieldMetadata } from '../../../../../types'; +import { SEARCH_FIELDS } from '../translations'; const search: Search = { box: { incremental: true, - placeholder: i18n.SEARCH_FIELDS, + placeholder: SEARCH_FIELDS, schema: true, }, }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/constants.ts new file mode 100644 index 0000000000000..45461a18c5f96 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_HISTORICAL_RESULTS_START_DATE = 'now-30d'; +export const DEFAULT_HISTORICAL_RESULTS_END_DATE = 'now'; + +export const ALL_TAB_ID = 'allTab' as const; +export const ECS_COMPLIANT_TAB_ID = 'ecsCompliantTab' as const; +export const CUSTOM_TAB_ID = 'customTab' as const; +export const INCOMPATIBLE_TAB_ID = 'incompatibleTab' as const; +export const SAME_FAMILY_TAB_ID = 'sameFamilyTab' as const; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.test.tsx similarity index 88% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.test.tsx index da16307b9a23b..c3ae2f2f579e7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.test.tsx @@ -8,8 +8,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { mockAllowedValues } from '../../../../../../../../../mock/allowed_values/mock_allowed_values'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; +import { mockAllowedValues } from '../../../../../mock/allowed_values/mock_allowed_values'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; import { EcsAllowedValues } from '.'; describe('EcsAllowedValues', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.tsx similarity index 85% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.tsx index 07197641c9788..69c46bba6a211 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/ecs_allowed_values/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/ecs_allowed_values/index.tsx @@ -8,9 +8,9 @@ import { EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { EMPTY_PLACEHOLDER } from '../helpers'; -import { CodeSuccess } from '../../../../../../../../../styles'; -import type { AllowedValue } from '../../../../../../../../../types'; +import { EMPTY_PLACEHOLDER } from '../../../../../constants'; +import { CodeSuccess } from '../../../../../styles'; +import type { AllowedValue } from '../../../../../types'; interface Props { allowedValues: AllowedValue[] | undefined; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_body.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_body/index.test.tsx similarity index 93% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_body.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_body/index.test.tsx index cd16e9870906a..94bbccb9e437a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_body.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_body/index.test.tsx @@ -8,7 +8,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { EmptyPromptBody } from './empty_prompt_body'; +import { EmptyPromptBody } from '.'; import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; describe('EmptyPromptBody', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_body.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_body/index.tsx similarity index 100% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_body.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_body/index.tsx diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_title.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_title/index.test.tsx similarity index 93% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_title.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_title/index.test.tsx index 0b146f7b7f123..a32f2bb50ebe2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_title.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_title/index.test.tsx @@ -8,7 +8,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { EmptyPromptTitle } from './empty_prompt_title'; +import { EmptyPromptTitle } from '.'; import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; describe('EmptyPromptTitle', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_title.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_title/index.tsx similarity index 100% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/empty_prompt_title.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/empty_prompt_title/index.tsx diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/constants.ts new file mode 100644 index 0000000000000..2866a35849f9b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_HISTORICAL_RESULTS_PAGE_SIZE = 10; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.test.tsx new file mode 100644 index 0000000000000..c2be83d369297 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + TestDataQualityProviders, + TestExternalProviders, + TestHistoricalResultsProvider, +} from '../../../../../../../../mock/test_providers/test_providers'; +import { getHistoricalResultStub } from '../../../../../../../../stub/get_historical_result_stub'; +import { HistoricalCheckFields } from '.'; + +describe('HistoricalCheckFields', () => { + it('should render incompatible (preselected) and same family field tabs', () => { + render( + + + + + + + + ); + + expect(screen.getByTestId('incompatibleTab')).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId('sameFamilyTab')).toHaveAttribute('aria-pressed', 'false'); + + expect(screen.getByTestId('incompatibleTabContent')).toBeInTheDocument(); + expect(screen.queryByTestId('sameFamilyTabContent')).not.toBeInTheDocument(); + }); + + describe('when clicking on tabs', () => { + it('should render respective tab content', async () => { + render( + + + + + + + + ); + + expect(screen.getByTestId('incompatibleTab')).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId('sameFamilyTab')).toHaveAttribute('aria-pressed', 'false'); + + expect(screen.getByTestId('incompatibleTabContent')).toBeInTheDocument(); + expect(screen.queryByTestId('sameFamilyTabContent')).not.toBeInTheDocument(); + + await act(async () => userEvent.click(screen.getByTestId('sameFamilyTab'))); + + expect(screen.getByTestId('incompatibleTab')).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId('sameFamilyTab')).toHaveAttribute('aria-pressed', 'true'); + + expect(screen.queryByTestId('incompatibleTabContent')).not.toBeInTheDocument(); + expect(screen.getByTestId('sameFamilyTabContent')).toBeInTheDocument(); + + await act(async () => userEvent.click(screen.getByTestId('incompatibleTab'))); + + expect(screen.getByTestId('incompatibleTab')).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId('sameFamilyTab')).toHaveAttribute('aria-pressed', 'false'); + + expect(screen.getByTestId('incompatibleTabContent')).toBeInTheDocument(); + expect(screen.queryByTestId('sameFamilyTabContent')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.tsx new file mode 100644 index 0000000000000..a8f206dec39a6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import type { NonLegacyHistoricalResult } from '../../../../../../../../types'; +import { getIncompatibleStatBadgeColor } from '../../../../../../../../utils/get_incompatible_stat_badge_color'; +import { INCOMPATIBLE_FIELDS, SAME_FAMILY } from '../../../../../../../../translations'; +import { INCOMPATIBLE_TAB_ID, SAME_FAMILY_TAB_ID } from '../../../../constants'; +import { getIncompatibleAndSameFamilyFieldsFromHistoricalResult } from './utils/get_incompatible_and_same_family_fields_from_historical_result'; +import { IncompatibleTab } from '../../../../incompatible_tab'; +import { SameFamilyTab } from '../../../../same_family_tab'; +import { CheckFieldsTabs } from '../../../../check_fields_tabs'; +import { StyledHistoricalResultsCheckFieldsButtonGroup } from '../styles'; + +export interface Props { + indexName: string; + historicalResult: NonLegacyHistoricalResult; +} + +const HistoricalCheckFieldsComponent: React.FC = ({ indexName, historicalResult }) => { + const { incompatibleMappingsFields, incompatibleValuesFields, sameFamilyFields } = + getIncompatibleAndSameFamilyFieldsFromHistoricalResult(historicalResult); + + const { + docsCount, + ilmPhase, + sizeInBytes, + incompatibleFieldCount, + sameFamilyFieldCount, + ecsFieldCount, + customFieldCount, + totalFieldCount, + } = historicalResult; + + const tabs = useMemo( + () => [ + { + id: INCOMPATIBLE_TAB_ID, + name: INCOMPATIBLE_FIELDS, + badgeColor: getIncompatibleStatBadgeColor(incompatibleFieldCount), + badgeCount: incompatibleFieldCount, + content: ( + + ), + }, + { + id: SAME_FAMILY_TAB_ID, + name: SAME_FAMILY, + badgeColor: getIncompatibleStatBadgeColor(sameFamilyFieldCount), + badgeCount: sameFamilyFieldCount, + content: ( + + ), + }, + ], + [ + customFieldCount, + docsCount, + ecsFieldCount, + ilmPhase, + incompatibleFieldCount, + incompatibleMappingsFields, + incompatibleValuesFields, + indexName, + sameFamilyFieldCount, + sameFamilyFields, + sizeInBytes, + totalFieldCount, + ] + ); + + return ( +
+ } + /> +
+ ); +}; + +HistoricalCheckFieldsComponent.displayName = 'HistoricalCheckFieldsComponent'; + +export const HistoricalCheckFields = React.memo(HistoricalCheckFieldsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.test.ts new file mode 100644 index 0000000000000..759875180fac8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getIncompatibleAndSameFamilyFieldsFromHistoricalResult } from './get_incompatible_and_same_family_fields_from_historical_result'; +import { EcsFlatTyped } from '../../../../../../../../../constants'; +import { getHistoricalResultStub } from '../../../../../../../../../stub/get_historical_result_stub'; + +describe('getIncompatibleAndSameFamilyFieldsFromHistoricalResult', () => { + it('should return incompatible and same family fields', () => { + const mockHistoricalResult = getHistoricalResultStub('test'); + const historicalResult = { + ...mockHistoricalResult, + incompatibleFieldMappingItems: [ + { + fieldName: 'host.name', + expectedValue: 'keyword', + actualValue: 'text', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + }, + ], + unallowedMappingFields: ['host.name'], + incompatibleFieldCount: 2, + sameFamilyFieldCount: 1, + sameFamilyFieldItems: [mockHistoricalResult.sameFamilyFieldItems[0]], + }; + + const result = getIncompatibleAndSameFamilyFieldsFromHistoricalResult(historicalResult); + + expect(result).toEqual( + expect.objectContaining({ + incompatibleMappingsFields: [ + { + ...EcsFlatTyped['host.name'], + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + ], + incompatibleValuesFields: [ + { + ...EcsFlatTyped['event.category'], + indexFieldName: 'event.category', + indexFieldType: 'keyword', + indexInvalidValues: [ + { + fieldName: 'siem', + count: 110616, + }, + ], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + ], + sameFamilyFields: [ + { + ...EcsFlatTyped['error.message'], + indexFieldName: 'error.message', + indexFieldType: 'match_only_text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }, + ], + }) + ); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.ts new file mode 100644 index 0000000000000..1cdd4a60a1675 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/historical_check_fields/utils/get_incompatible_and_same_family_fields_from_historical_result.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsFlatTyped } from '../../../../../../../../../constants'; +import type { + IncompatibleFieldMetadata, + NonLegacyHistoricalResult, + SameFamilyFieldMetadata, +} from '../../../../../../../../../types'; + +interface IncompatibleAndSameFamilyFields { + incompatibleMappingsFields: IncompatibleFieldMetadata[]; + incompatibleValuesFields: IncompatibleFieldMetadata[]; + sameFamilyFields: SameFamilyFieldMetadata[]; +} + +export const getIncompatibleAndSameFamilyFieldsFromHistoricalResult = ( + historicalResult: NonLegacyHistoricalResult +): IncompatibleAndSameFamilyFields => { + const incompatibleAndSameFamilyFields: IncompatibleAndSameFamilyFields = { + incompatibleMappingsFields: [], + incompatibleValuesFields: [], + sameFamilyFields: [], + }; + + const { incompatibleFieldMappingItems, incompatibleFieldValueItems, sameFamilyFieldItems } = + historicalResult; + + for (const incompatibleFieldMappingItem of incompatibleFieldMappingItems) { + const { fieldName, actualValue } = incompatibleFieldMappingItem; + const incompatibleMappingsField: IncompatibleFieldMetadata = { + ...EcsFlatTyped[fieldName], + indexFieldName: fieldName, + indexFieldType: actualValue, + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }; + + incompatibleAndSameFamilyFields.incompatibleMappingsFields.push(incompatibleMappingsField); + } + + for (const incompatibleFieldValueItem of incompatibleFieldValueItems) { + const { fieldName, actualValues } = incompatibleFieldValueItem; + const incompatibleValuesField: IncompatibleFieldMetadata = { + ...EcsFlatTyped[fieldName], + indexFieldName: fieldName, + indexFieldType: EcsFlatTyped[fieldName].type, + indexInvalidValues: actualValues.map((actualValue) => ({ + fieldName: actualValue.name, + count: actualValue.count, + })), + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }; + + incompatibleAndSameFamilyFields.incompatibleValuesFields.push(incompatibleValuesField); + } + + for (const sameFamilyFieldItem of sameFamilyFieldItems) { + const { fieldName, expectedValue } = sameFamilyFieldItem; + const sameFamilyField: SameFamilyFieldMetadata = { + ...EcsFlatTyped[fieldName], + indexFieldName: fieldName, + indexFieldType: expectedValue, + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }; + + incompatibleAndSameFamilyFields.sameFamilyFields.push(sameFamilyField); + } + + return incompatibleAndSameFamilyFields; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.test.tsx new file mode 100644 index 0000000000000..59d83580197e9 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; + +import { HistoricalResult } from '.'; +import { + getHistoricalResultStub, + getLegacyHistoricalResultStub, +} from '../../../../../../../stub/get_historical_result_stub'; +import { + TestDataQualityProviders, + TestExternalProviders, + TestHistoricalResultsProvider, +} from '../../../../../../../mock/test_providers/test_providers'; + +describe('HisoricalResult', () => { + it('should render extended index stats panel', () => { + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('indexStatsPanel'); + expect(wrapper).toBeInTheDocument(); + expect(wrapper.textContent).toBe( + 'Docs618,675ILM phaseunmanagedSize81.2MBCustom fields64ECS compliant fields44All fields112' + ); + }); + + it('should render historical check fields', () => { + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('historicalCheckFields'); + expect(wrapper).toBeInTheDocument(); + }); + + describe('when historical result is legacy', () => { + it('should render legacy historical check fields', () => { + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('legacyHistoricalCheckFields'); + expect(wrapper).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.tsx new file mode 100644 index 0000000000000..43f7bcc58b360 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; + +import type { HistoricalResult as HistoricalResultType } from '../../../../../../../types'; +import { IndexStatsPanel } from '../../../index_stats_panel'; +import { HistoricalCheckFields } from './historical_check_fields'; +// eslint-disable-next-line no-restricted-imports +import { isNonLegacyHistoricalResult } from './utils/is_non_legacy_historical_result'; +// eslint-disable-next-line no-restricted-imports +import { LegacyHistoricalCheckFields } from './legacy_historical_check_fields'; + +export interface Props { + indexName: string; + historicalResult: HistoricalResultType; +} + +const HistoricalResultComponent: React.FC = ({ indexName, historicalResult }) => { + const { + docsCount, + sizeInBytes, + ilmPhase, + ecsFieldCount, + totalFieldCount, + customFieldCount, + checkedAt, + } = historicalResult; + + return ( +
+ + + + {isNonLegacyHistoricalResult(historicalResult) ? ( + + ) : ( + + )} + +
+ ); +}; + +HistoricalResultComponent.displayName = 'HistoricalResultComponent'; + +export const HistoricalResult = React.memo(HistoricalResultComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.test.tsx new file mode 100644 index 0000000000000..d232db88e5a64 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render, act } from '@testing-library/react'; +import { EuiMarkdownFormat, copyToClipboard } from '@elastic/eui'; +import userEvent from '@testing-library/user-event'; + +import { getLegacyHistoricalResultStub } from '../../../../../../../../stub/get_historical_result_stub'; +import { LegacyHistoricalCheckFields } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../../../../../mock/test_providers/test_providers'; + +jest.mock('@elastic/eui', () => { + const originalModule = jest.requireActual('@elastic/eui'); + + return { + ...originalModule, + copyToClipboard: jest.fn(), + }; +}); + +// This function is used to strip all attributes from an HTML string +// so that we can compare the rendered HTML without worrying about unstable attributes +// useful for crude markdown rendering comparison +function stripAttributes(html: string) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + function removeAttributes(node: ChildNode) { + if (node.nodeType === Node.ELEMENT_NODE) { + if (!(node instanceof Element)) throw new Error(); + while (node.attributes.length > 0) { + node.removeAttribute(node.attributes[0].name); + } + node.childNodes.forEach(removeAttributes); + } + } + + removeAttributes(doc.body); + return doc.body.innerHTML; +} + +describe('LegacyHistoricalCheckFields', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('should render incompatible (preselected) and disabled same family field tabs', () => { + render( + + + + + + ); + + expect(screen.getByTestId('incompatibleTab')).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId('sameFamilyTab')).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByTestId('sameFamilyTab')).toBeDisabled(); + + expect(screen.getByTestId('legacyIncompatibleTabContent')).toBeInTheDocument(); + expect(screen.queryByTestId('sameFamilyTabContent')).not.toBeInTheDocument(); + }); + + describe('incompatible fields tab', () => { + describe('when there are incompatible fields', () => { + it('should render incompatible fields messages including ecs version and field count', () => { + const historicalResult = getLegacyHistoricalResultStub('test'); + render( + + + + + + ); + + expect(screen.getByTestId('incompatibleCallout')).toHaveTextContent( + `${historicalResult.incompatibleFieldCount} incompatible field` + ); + expect(screen.getByTestId('fieldsAreIncompatible')).toHaveTextContent( + `Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${historicalResult.ecsVersion}.` + ); + }); + + it('should render eui formatted markdown from markdownComments table slice', () => { + const historicalResult = getLegacyHistoricalResultStub('test'); + render( + + + + + + ); + + const tablesMarkdown = historicalResult.markdownComments.slice(4).join('\n'); + + const wrapper = screen.getByTestId('incompatibleTablesMarkdown'); + + const actualHTML = stripAttributes(wrapper.outerHTML); + const expectedHTML = stripAttributes( + render( + + {tablesMarkdown} + + ).container.innerHTML + ); + + expect(screen.getByTestId('incompatibleTablesMarkdown')).toBeInTheDocument(); + expect(actualHTML).toBe(expectedHTML); + }); + + it('should render full actions consuming full markdown comment', async () => { + const historicalResult = getLegacyHistoricalResultStub('test'); + + const openCreateCaseFlyout = jest.fn(); + const { markdownComments } = historicalResult; + + render( + + + + + + ); + + const wrapper = screen.getByTestId('actions'); + + expect(wrapper).toBeInTheDocument(); + + const addToNewCase = screen.getByLabelText('Add to new case'); + expect(addToNewCase).toBeInTheDocument(); + await act(async () => userEvent.click(addToNewCase)); + expect(openCreateCaseFlyout).toHaveBeenCalledWith({ + comments: [markdownComments.join('\n')], + headerContent: expect.anything(), + }); + + const copyToClipboardElement = screen.getByLabelText('Copy to clipboard'); + expect(copyToClipboardElement).toBeInTheDocument(); + await act(async () => userEvent.click(copyToClipboardElement)); + expect(copyToClipboard).toHaveBeenCalledWith(markdownComments.join('\n')); + + const chat = screen.getByTestId('newChatLink'); + expect(chat).toBeInTheDocument(); + // clicking in test is broken atm + // so can't test the chat action + markdown comment + }); + }); + }); + + describe('same family tab', () => { + it('should have warning tooltip', async () => { + render( + + + + + + ); + + expect(screen.getByTestId('disabledReasonTooltip')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.tsx new file mode 100644 index 0000000000000..177ba83110852 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, memo, useMemo } from 'react'; +import { EuiMarkdownFormat, EuiSpacer } from '@elastic/eui'; + +import { INCOMPATIBLE_FIELDS, SAME_FAMILY } from '../../../../../../../../translations'; +import { Actions } from '../../../../../../../../actions'; +import { LegacyHistoricalResult } from '../../../../../../../../types'; +import { IncompatibleCallout } from '../../../../incompatible_callout'; +import { CheckSuccessEmptyPrompt } from '../../../../check_success_empty_prompt'; +import { INCOMPATIBLE_TAB_ID, SAME_FAMILY_TAB_ID } from '../../../../constants'; +import { getIncompatibleStatBadgeColor } from '../../../../../../../../utils/get_incompatible_stat_badge_color'; +import { CheckFieldsTabs } from '../../../../check_fields_tabs'; +import { StyledHistoricalResultsCheckFieldsButtonGroup } from '../styles'; +import { NOT_INCLUDED_IN_HISTORICAL_RESULTS } from './translations'; + +interface Props { + indexName: string; + historicalResult: LegacyHistoricalResult; +} + +const LegacyHistoricalCheckFieldsComponent: FC = ({ indexName, historicalResult }) => { + const { markdownComments, incompatibleFieldCount, ecsVersion, sameFamilyFieldCount } = + historicalResult; + + const markdownComment = useMemo(() => markdownComments.join('\n'), [markdownComments]); + const tablesComment = useMemo(() => markdownComments.slice(4).join('\n'), [markdownComments]); + + const tabs = useMemo( + () => [ + { + id: INCOMPATIBLE_TAB_ID, + name: INCOMPATIBLE_FIELDS, + badgeColor: getIncompatibleStatBadgeColor(incompatibleFieldCount), + badgeCount: incompatibleFieldCount, + content: ( +
+ {incompatibleFieldCount > 0 ? ( + <> + + + + {tablesComment} + + + + + ) : ( + + )} +
+ ), + }, + { + id: SAME_FAMILY_TAB_ID, + name: SAME_FAMILY, + badgeColor: 'hollow', + badgeCount: sameFamilyFieldCount, + disabled: true, + disabledReason: NOT_INCLUDED_IN_HISTORICAL_RESULTS, + }, + ], + [ + ecsVersion, + incompatibleFieldCount, + indexName, + markdownComment, + sameFamilyFieldCount, + tablesComment, + ] + ); + + return ( +
+ } + /> +
+ ); +}; + +LegacyHistoricalCheckFieldsComponent.displayName = 'LegacyHistoricalCheckFieldsComponent'; + +export const LegacyHistoricalCheckFields = memo(LegacyHistoricalCheckFieldsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/translations.ts new file mode 100644 index 0000000000000..6f61a1b13c40f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/legacy_historical_check_fields/translations.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NOT_INCLUDED_IN_HISTORICAL_RESULTS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.notIncludedInHistoricalResults', + { + defaultMessage: + 'Not included in historical results. To see full data about same family fields, run a new check.', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/styles.tsx new file mode 100644 index 0000000000000..8ae6a86dda62c --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/styles.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; +import { EuiButtonGroup } from '@elastic/eui'; + +import { INCOMPATIBLE_TAB_ID, SAME_FAMILY_TAB_ID } from '../../../constants'; + +export const StyledHistoricalResultsCheckFieldsButtonGroup = styled(EuiButtonGroup)` + min-width: 50%; + button[data-test-subj='${INCOMPATIBLE_TAB_ID}'] { + flex-grow: 1; + } + button[data-test-subj='${SAME_FAMILY_TAB_ID}'] { + flex-grow: 1; + } +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.test.ts new file mode 100644 index 0000000000000..55c7bb91c5b2b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getHistoricalResultStub } from '../../../../../../../../stub/get_historical_result_stub'; +// eslint-disable-next-line no-restricted-imports +import { isNonLegacyHistoricalResult } from './is_non_legacy_historical_result'; + +describe('isNonLegacyHistoricalResult', () => { + describe('given legacy historical result', () => { + it('should return false', () => { + const { + incompatibleFieldMappingItems, + incompatibleFieldValueItems, + sameFamilyFieldItems, + ...legacyHistoricalResult + } = getHistoricalResultStub('test'); + + const result = isNonLegacyHistoricalResult(legacyHistoricalResult); + + expect(result).toBe(false); + }); + }); + + describe('given non-legacy historical result', () => { + it('should return true', () => { + const historicalResult = getHistoricalResultStub('test'); + + const result = isNonLegacyHistoricalResult(historicalResult); + + expect(result).toBe(true); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.ts new file mode 100644 index 0000000000000..9160e54d461e9 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/historical_result/utils/is_non_legacy_historical_result.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HistoricalResult, NonLegacyHistoricalResult } from '../../../../../../../../types'; + +export const isNonLegacyHistoricalResult = ( + historicalResult: HistoricalResult +): historicalResult is NonLegacyHistoricalResult => { + return ( + 'incompatibleFieldMappingItems' in historicalResult && + 'incompatibleFieldValueItems' in historicalResult && + 'sameFamilyFieldItems' in historicalResult + ); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.test.tsx new file mode 100644 index 0000000000000..78feadadc4d13 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.test.tsx @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; + +import { generateHistoricalResultsStub } from '../../../../../../stub/generate_historical_results_stub'; +import { + TestDataQualityProviders, + TestExternalProviders, + TestHistoricalResultsProvider, +} from '../../../../../../mock/test_providers/test_providers'; +import { HistoricalResultsList } from '.'; +import { + CHANGE_YOUR_SEARCH_CRITERIA_OR_RUN, + NO_RESULTS_MATCH_YOUR_SEARCH_CRITERIA, + TOGGLE_HISTORICAL_RESULT_CHECKED_AT, +} from './translations'; +import { getFormattedCheckTime } from '../../utils/get_formatted_check_time'; +import { getHistoricalResultStub } from '../../../../../../stub/get_historical_result_stub'; +import userEvent from '@testing-library/user-event'; + +const getAccordionToggleLabel = (checkedAt: number) => { + return TOGGLE_HISTORICAL_RESULT_CHECKED_AT(getFormattedCheckTime(checkedAt)); +}; + +describe('HistoricalResultsList', () => { + it('should render individual historical result accordions with result outcome text, formatted check time and amount of incompatible fields', () => { + const indexName = 'test'; + const [historicalResultFail, historicalResultPass] = generateHistoricalResultsStub( + indexName, + 2 + ); + const modifiedResults = [ + historicalResultFail, + { + ...historicalResultPass, + totalFieldCount: historicalResultPass.totalFieldCount - 1, + incompatibleFieldCount: 0, + incompatibleFieldMappingItems: [], + incompatibleFieldValueItems: [], + markdownComments: [], + }, + ]; + + render( + + + + + + + + ); + + expect( + screen.getByLabelText(getAccordionToggleLabel(historicalResultFail.checkedAt)) + ).toHaveTextContent( + `Fail${getFormattedCheckTime(historicalResultFail.checkedAt)}1 Incompatible field` + ); + + expect( + screen.getByLabelText(getAccordionToggleLabel(historicalResultPass.checkedAt)) + ).toHaveTextContent( + `Pass${getFormattedCheckTime(historicalResultPass.checkedAt)}0 Incompatible fields` + ); + }); + + it('should render historical results accordions collapsed', () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + render( + + + + + + + + ); + + const accordionToggleButton = screen.getByRole('button', { + name: TOGGLE_HISTORICAL_RESULT_CHECKED_AT(getFormattedCheckTime(historicalResult.checkedAt)), + }); + + expect(accordionToggleButton).toBeInTheDocument(); + + expect(accordionToggleButton).toHaveAttribute('aria-expanded', 'false'); + }); + + describe('when historical result is expanded', () => { + it('should remove incompatible field count text', async () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + render( + + + + + + + + ); + + const accordionToggleButton = screen.getByRole('button', { + name: TOGGLE_HISTORICAL_RESULT_CHECKED_AT( + getFormattedCheckTime(historicalResult.checkedAt) + ), + }); + + expect(accordionToggleButton).toBeInTheDocument(); + + await act(async () => userEvent.click(accordionToggleButton)); + + expect(accordionToggleButton).toHaveAttribute('aria-expanded', 'true'); + + const accordionToggleDiv = screen.getByLabelText( + getAccordionToggleLabel(historicalResult.checkedAt) + ); + + expect(accordionToggleDiv).toHaveTextContent( + `Fail${getFormattedCheckTime(historicalResult.checkedAt)}` + ); + + expect(accordionToggleDiv).not.toHaveTextContent('1 Incompatible field'); + }); + }); + + describe('when historical results are empty', () => { + it('should show empty message', () => { + const indexName = 'test'; + render( + + + + + + + + ); + + expect(screen.getByText(NO_RESULTS_MATCH_YOUR_SEARCH_CRITERIA)).toBeInTheDocument(); + expect(screen.getByText(CHANGE_YOUR_SEARCH_CRITERIA_OR_RUN)).toBeInTheDocument(); + }); + }); + + describe('when clicked on each accordion', () => { + it('should expand each accordion independently', async () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 2); + render( + + + + + + + + ); + + for (const result of results) { + const accordionToggleButton = screen.getByRole('button', { + name: TOGGLE_HISTORICAL_RESULT_CHECKED_AT(getFormattedCheckTime(result.checkedAt)), + }); + + expect(accordionToggleButton).toBeInTheDocument(); + + expect(accordionToggleButton).toHaveAttribute('aria-expanded', 'false'); + + await act(async () => userEvent.click(accordionToggleButton)); + } + + const allAccordionToggles = screen.getAllByRole('button', { + name: /Toggle historical result checked at/, + }); + + for (const accordionToggleButton of allAccordionToggles) { + expect(accordionToggleButton).toHaveAttribute('aria-expanded', 'true'); + } + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.tsx new file mode 100644 index 0000000000000..cabe0b26f8bac --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/index.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, Fragment, memo, useState } from 'react'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTextColor, + useGeneratedHtmlId, +} from '@elastic/eui'; + +import { useDataQualityContext } from '../../../../../../data_quality_context'; +import { useHistoricalResultsContext } from '../../../contexts/historical_results_context'; +import { StyledAccordion } from './styles'; +import { getFormattedCheckTime } from '../../utils/get_formatted_check_time'; +import { IndexResultBadge } from '../../../index_result_badge'; +import { HistoricalResult } from './historical_result'; +import { StyledText } from '../styles'; +import { getCheckTextColor } from '../../../utils/get_check_text_color'; +import { + CHANGE_YOUR_SEARCH_CRITERIA_OR_RUN, + COUNTED_INCOMPATIBLE_FIELDS, + NO_RESULTS_MATCH_YOUR_SEARCH_CRITERIA, + TOGGLE_HISTORICAL_RESULT_CHECKED_AT, +} from './translations'; + +interface Props { + indexName: string; +} + +export const HistoricalResultsListComponent: FC = ({ indexName }) => { + const [accordionState, setAccordionState] = useState>(() => ({})); + const historicalResultsAccordionId = useGeneratedHtmlId({ prefix: 'historicalResultsAccordion' }); + const { historicalResultsState } = useHistoricalResultsContext(); + + const { formatNumber } = useDataQualityContext(); + + const { results } = historicalResultsState; + + return ( +
+ {results.length > 0 ? ( + <> + {results.map((result) => ( + + + { + setAccordionState((prevState) => ({ + ...prevState, + [result.checkedAt]: isOpen, + })); + }} + buttonContent={ + + + + + + {getFormattedCheckTime(result.checkedAt)} + + {!accordionState[result.checkedAt] && ( + + + + {formatNumber(result.incompatibleFieldCount)} + {' '} + {COUNTED_INCOMPATIBLE_FIELDS(result.incompatibleFieldCount)} + + + )} + + } + > + + + + ))} + + ) : ( + {NO_RESULTS_MATCH_YOUR_SEARCH_CRITERIA}} + body={

{CHANGE_YOUR_SEARCH_CRITERIA_OR_RUN}

} + /> + )} +
+ ); +}; + +HistoricalResultsListComponent.displayName = 'HistoricalResultsListComponent'; + +export const HistoricalResultsList = memo(HistoricalResultsListComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/styles.tsx new file mode 100644 index 0000000000000..7109a1efa07bf --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/styles.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiAccordion } from '@elastic/eui'; +import styled from 'styled-components'; + +export const StyledAccordion = styled(EuiAccordion)` + padding: 14px ${({ theme }) => theme.eui.euiSize}; + border: 1px solid ${({ theme }) => theme.eui.euiBorderColor}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + + .euiAccordion__button:is(:hover, :focus) { + text-decoration: none; + } + + .euiAccordion__buttonContent { + flex-grow: 1; + } +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/translations.ts new file mode 100644 index 0000000000000..fd6a6b74e6c0d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/historical_results_list/translations.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_RESULTS_MATCH_YOUR_SEARCH_CRITERIA = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.noResultsMatchYourSearchCriteria', + { + defaultMessage: 'No results match your search criteria', + } +); + +export const CHANGE_YOUR_SEARCH_CRITERIA_OR_RUN = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.changeYourSearchCriteriaOrRun', + { + defaultMessage: 'Change your search criteria or run a new check', + } +); + +export const TOGGLE_HISTORICAL_RESULT_CHECKED_AT = (checkedAt: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.toggleHistoricalResultCheckedAt', + { + values: { + checkedAt, + }, + defaultMessage: 'Toggle historical result checked at {checkedAt}', + } + ); + +export const COUNTED_INCOMPATIBLE_FIELDS = (count: number) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldsWithCount', { + values: { + count, + }, + defaultMessage: '{count, plural, one {Incompatible field} other {Incompatible fields}}', + }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_date_picker/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_date_picker/index.tsx new file mode 100644 index 0000000000000..cc4d3e0345e93 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_date_picker/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { OnTimeChangeProps } from '@elastic/eui'; + +import { useAbortControllerRef } from '../../../../../../../hooks/use_abort_controller_ref'; +import { useIsMountedRef } from '../../../../../../../hooks/use_is_mounted_ref'; +import { FetchHistoricalResultsQueryState } from '../../../types'; +import { FetchHistoricalResultsQueryAction } from '../../types'; +import { useHistoricalResultsContext } from '../../../../contexts/historical_results_context'; + +export interface UseHistoricalResultsDatePickerOpts { + indexName: string; + fetchHistoricalResultsQueryState: FetchHistoricalResultsQueryState; + fetchHistoricalResultsQueryDispatch: React.Dispatch; +} + +export interface UseHistoricalResultsDatePickerReturnValue { + handleTimeChange: ({ start, end, isInvalid }: OnTimeChangeProps) => Promise; +} + +export const useHistoricalResultsDatePicker = ({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, +}: UseHistoricalResultsDatePickerOpts): UseHistoricalResultsDatePickerReturnValue => { + const fetchHistoricalResultsFromDateAbortControllerRef = useAbortControllerRef(); + const { fetchHistoricalResults } = useHistoricalResultsContext(); + const { isMountedRef } = useIsMountedRef(); + + const handleTimeChange = useCallback( + async ({ start, end, isInvalid }: OnTimeChangeProps) => { + if (isInvalid) { + return; + } + + await fetchHistoricalResults({ + abortController: fetchHistoricalResultsFromDateAbortControllerRef.current, + indexName, + size: fetchHistoricalResultsQueryState.size, + from: 0, + startDate: start, + endDate: end, + ...(fetchHistoricalResultsQueryState.outcome && { + outcome: fetchHistoricalResultsQueryState.outcome, + }), + }); + + if (isMountedRef.current) { + fetchHistoricalResultsQueryDispatch({ + type: 'SET_DATE', + payload: { startDate: start, endDate: end }, + }); + } + }, + [ + fetchHistoricalResults, + fetchHistoricalResultsFromDateAbortControllerRef, + fetchHistoricalResultsQueryDispatch, + fetchHistoricalResultsQueryState.outcome, + fetchHistoricalResultsQueryState.size, + indexName, + isMountedRef, + ] + ); + + return { + handleTimeChange, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_outcome_filter/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_outcome_filter/index.tsx new file mode 100644 index 0000000000000..23ab524df5c40 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_outcome_filter/index.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useCallback } from 'react'; + +import { useAbortControllerRef } from '../../../../../../../hooks/use_abort_controller_ref'; +import { useHistoricalResultsContext } from '../../../../contexts/historical_results_context'; +import { FetchHistoricalResultsQueryState, UseHistoricalResultsFetchOpts } from '../../../types'; +import { FetchHistoricalResultsQueryAction } from '../../types'; +import { useIsMountedRef } from '../../../../../../../hooks/use_is_mounted_ref'; + +export interface UseHistoricalResultsOutcomeFilterOpts { + indexName: string; + fetchHistoricalResultsQueryState: FetchHistoricalResultsQueryState; + fetchHistoricalResultsQueryDispatch: React.Dispatch; +} + +export interface UseHistoricalResultsOutcomeFilterReturnValue { + handleDefaultOutcome: () => void; + handlePassOutcome: () => void; + handleFailOutcome: () => void; + isShowAll: boolean; + isShowPass: boolean; + isShowFail: boolean; +} + +export const useHistoricalResultsOutcomeFilter = ({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, +}: UseHistoricalResultsOutcomeFilterOpts): UseHistoricalResultsOutcomeFilterReturnValue => { + const fetchHistoricalResultsFromOutcomeAbortControllerRef = useAbortControllerRef(); + const { isMountedRef } = useIsMountedRef(); + const { fetchHistoricalResults } = useHistoricalResultsContext(); + + const handleOutcomeFilterChange = useCallback( + async (outcome: 'pass' | 'fail' | undefined) => { + const opts: UseHistoricalResultsFetchOpts = { + indexName, + abortController: fetchHistoricalResultsFromOutcomeAbortControllerRef.current, + size: fetchHistoricalResultsQueryState.size, + from: 0, + startDate: fetchHistoricalResultsQueryState.startDate, + endDate: fetchHistoricalResultsQueryState.endDate, + }; + + if (outcome != null) { + opts.outcome = outcome; + } + + await fetchHistoricalResults(opts); + + if (isMountedRef.current) { + fetchHistoricalResultsQueryDispatch({ type: 'SET_OUTCOME', payload: outcome }); + } + }, + [ + fetchHistoricalResults, + fetchHistoricalResultsFromOutcomeAbortControllerRef, + fetchHistoricalResultsQueryDispatch, + fetchHistoricalResultsQueryState.endDate, + fetchHistoricalResultsQueryState.size, + fetchHistoricalResultsQueryState.startDate, + indexName, + isMountedRef, + ] + ); + + const handleDefaultOutcome = useCallback(() => { + handleOutcomeFilterChange(undefined); + }, [handleOutcomeFilterChange]); + + const handlePassOutcome = useCallback(() => { + handleOutcomeFilterChange('pass'); + }, [handleOutcomeFilterChange]); + + const handleFailOutcome = useCallback(() => { + handleOutcomeFilterChange('fail'); + }, [handleOutcomeFilterChange]); + + const isShowAll = fetchHistoricalResultsQueryState.outcome == null; + const isShowPass = fetchHistoricalResultsQueryState.outcome === 'pass'; + const isShowFail = fetchHistoricalResultsQueryState.outcome === 'fail'; + + return { + handleDefaultOutcome, + handlePassOutcome, + handleFailOutcome, + isShowAll, + isShowPass, + isShowFail, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/index.tsx new file mode 100644 index 0000000000000..4f7038fe385d0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/index.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useCallback, useReducer } from 'react'; + +import { useIsMountedRef } from '../../../../../../../hooks/use_is_mounted_ref'; +import { useAbortControllerRef } from '../../../../../../../hooks/use_abort_controller_ref'; +import { DEFAULT_HISTORICAL_RESULTS_PAGE_SIZE } from '../../constants'; +import { FetchHistoricalResultsQueryAction, PaginationReducerState } from '../../types'; +import { useHistoricalResultsContext } from '../../../../contexts/historical_results_context'; +import { historicalResultsPaginationReducer } from './reducers/historical_results_pagination_reducer'; +import { FetchHistoricalResultsQueryState } from '../../../types'; + +export const initialPaginationState: PaginationReducerState = { + activePage: 0, + pageCount: 1, + rowSize: DEFAULT_HISTORICAL_RESULTS_PAGE_SIZE, +}; + +export interface UseHistoricalResultsPaginationOpts { + indexName: string; + fetchHistoricalResultsQueryState: FetchHistoricalResultsQueryState; + fetchHistoricalResultsQueryDispatch: React.Dispatch; +} + +export interface UseHistoricalResultsPaginationReturnValue { + paginationState: PaginationReducerState; + handleChangeItemsPerPage: (rowSize: number) => Promise; + handleChangeActivePage: (nextPageIndex: number) => Promise; +} + +export const useHistoricalResultsPagination = ({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, +}: UseHistoricalResultsPaginationOpts): UseHistoricalResultsPaginationReturnValue => { + const fetchHistoricalResultsFromSetPageAbortControllerRef = useAbortControllerRef(); + const fetchHistoricalResultsFromSetSizeAbortControllerRef = useAbortControllerRef(); + const { isMountedRef } = useIsMountedRef(); + + const { historicalResultsState, fetchHistoricalResults } = useHistoricalResultsContext(); + + const [paginationState, paginationDispatch] = useReducer( + historicalResultsPaginationReducer, + initialPaginationState + ); + // Ensure pagination state updates when total results change externally. + // Avoid moving everything into useEffect to prevent confusion and potential infinite rerender bugs. + // Keep only the necessary minimum in useEffect. + useEffect(() => { + paginationDispatch({ + type: 'SET_ROW_SIZE', + payload: { + rowSize: paginationState.rowSize, + totalResults: historicalResultsState.total, + }, + }); + }, [historicalResultsState.total, paginationDispatch, paginationState.rowSize]); + + const handleChangeItemsPerPage = useCallback( + async (rowSize: number) => { + await fetchHistoricalResults({ + indexName, + abortController: fetchHistoricalResultsFromSetSizeAbortControllerRef.current, + startDate: fetchHistoricalResultsQueryState.startDate, + endDate: fetchHistoricalResultsQueryState.endDate, + from: 0, + size: rowSize, + ...(fetchHistoricalResultsQueryState.outcome && { + outcome: fetchHistoricalResultsQueryState.outcome, + }), + }); + + if (isMountedRef.current) { + fetchHistoricalResultsQueryDispatch({ type: 'SET_SIZE', payload: rowSize }); + paginationDispatch({ + type: 'SET_ROW_SIZE', + payload: { + rowSize, + totalResults: historicalResultsState.total, + }, + }); + } + }, + [ + fetchHistoricalResults, + fetchHistoricalResultsFromSetSizeAbortControllerRef, + fetchHistoricalResultsQueryDispatch, + fetchHistoricalResultsQueryState.endDate, + fetchHistoricalResultsQueryState.outcome, + fetchHistoricalResultsQueryState.startDate, + historicalResultsState.total, + indexName, + isMountedRef, + ] + ); + + const handleChangeActivePage = useCallback( + async (nextPageIndex: number) => { + const size = fetchHistoricalResultsQueryState.size; + const nextFrom = nextPageIndex * size; + + await fetchHistoricalResults({ + indexName, + abortController: fetchHistoricalResultsFromSetPageAbortControllerRef.current, + size, + startDate: fetchHistoricalResultsQueryState.startDate, + endDate: fetchHistoricalResultsQueryState.endDate, + from: nextFrom, + ...(fetchHistoricalResultsQueryState.outcome && { + outcome: fetchHistoricalResultsQueryState.outcome, + }), + }); + + if (isMountedRef.current) { + fetchHistoricalResultsQueryDispatch({ type: 'SET_FROM', payload: nextFrom }); + paginationDispatch({ type: 'SET_ACTIVE_PAGE', payload: nextPageIndex }); + } + }, + [ + fetchHistoricalResults, + fetchHistoricalResultsFromSetPageAbortControllerRef, + fetchHistoricalResultsQueryDispatch, + fetchHistoricalResultsQueryState.endDate, + fetchHistoricalResultsQueryState.outcome, + fetchHistoricalResultsQueryState.size, + fetchHistoricalResultsQueryState.startDate, + indexName, + isMountedRef, + ] + ); + + return { + paginationState, + handleChangeItemsPerPage, + handleChangeActivePage, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.test.ts new file mode 100644 index 0000000000000..43f344be074ce --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { historicalResultsPaginationReducer } from './historical_results_pagination_reducer'; + +describe('historicalResultsPaginationReducer', () => { + describe('on SET_ROW_SIZE', () => { + it('should set rowSize and pageCount and reset activePage to 0', () => { + const state = { rowSize: 10, pageCount: 1, activePage: 0 }; + const action = { type: 'SET_ROW_SIZE' as const, payload: { rowSize: 5, totalResults: 11 } }; + const newState = historicalResultsPaginationReducer(state, action); + expect(newState).toEqual({ rowSize: 5, pageCount: 3, activePage: 0 }); + }); + }); + + describe('on SET_ACTIVE_PAGE', () => { + it('should set activePage', () => { + const state = { rowSize: 10, pageCount: 1, activePage: 0 }; + const action = { type: 'SET_ACTIVE_PAGE' as const, payload: 1 }; + const newState = historicalResultsPaginationReducer(state, action); + expect(newState).toEqual({ rowSize: 10, pageCount: 1, activePage: 1 }); + }); + }); + + describe('on unknown action', () => { + it('should return the state', () => { + const state = { rowSize: 10, pageCount: 1, activePage: 0 }; + const action = { type: 'UNKNOWN_ACTION' }; + // @ts-expect-error + const newState = historicalResultsPaginationReducer(state, action); + expect(newState).toEqual(state); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.ts new file mode 100644 index 0000000000000..4f9829ddb3bc3 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/hooks/use_historical_results_pagination/reducers/historical_results_pagination_reducer.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PaginationReducerAction, PaginationReducerState } from '../../../types'; + +export const historicalResultsPaginationReducer = ( + state: PaginationReducerState, + action: PaginationReducerAction +) => { + if (action.type === 'SET_ROW_SIZE') { + return { + rowSize: action.payload.rowSize, + // reason for Math.ceil is to ensure that we have a page for the remaining results + // e.g. if we have 11 results and rowSize is 5, 11/5 = 2.2, if we use Math.floor we will have 2 pages + // and will miss the last result, so we use Math.ceil to have 3 pages to include the last result + pageCount: Math.ceil(action.payload.totalResults / action.payload.rowSize), + // reset activePage to 0 when rowSize changes + // because our activePage can be greater than the new pageCount + activePage: 0, + }; + } + + if (action.type === 'SET_ACTIVE_PAGE') { + return { + ...state, + activePage: action.payload, + }; + } + + return state; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.test.tsx new file mode 100644 index 0000000000000..7c0b13f094030 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.test.tsx @@ -0,0 +1,466 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { HistoricalResults } from '.'; +import { screen, render, within, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + TestDataQualityProviders, + TestExternalProviders, + TestHistoricalResultsProvider, +} from '../../../../../mock/test_providers/test_providers'; +import { getHistoricalResultStub } from '../../../../../stub/get_historical_result_stub'; +import { + ERROR_LOADING_HISTORICAL_RESULTS, + FILTER_RESULTS_BY_OUTCOME, + LOADING_HISTORICAL_RESULTS, +} from './translations'; +import { generateHistoricalResultsStub } from '../../../../../stub/generate_historical_results_stub'; + +describe('HistoricalResults', () => { + it('should render historical results list', () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 2); + render( + + + + + + + + ); + + expect(screen.getByRole('status', { name: '2 checks' })).toBeInTheDocument(); + expect(screen.getByTestId('historicalResultsList')).toBeInTheDocument(); + }); + + describe('Outcome Filter', () => { + it('should render outcome filters block with no specific outcome preselected', () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + render( + + + + + + + + ); + + expect( + screen.getByRole('radiogroup', { name: FILTER_RESULTS_BY_OUTCOME }) + ).toBeInTheDocument(); + const outcomeFilterAll = screen.getByRole('radio', { name: 'All' }); + + expect(outcomeFilterAll).toBeInTheDocument(); + expect(outcomeFilterAll).toHaveAttribute('aria-checked', 'true'); + }); + }); + + describe.each(['All', 'Pass', 'Fail'])('when %s outcome filter is clicked', (outcome) => { + it(`should invoke fetchHistoricalResults with ${outcome} outcome, from: 0 and remaining fetch query opts`, async () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + const fetchHistoricalResults = jest.fn(); + render( + + + + + + + + ); + + const outcomeFilter = screen.getByRole('radio', { name: outcome }); + await act(async () => outcomeFilter.click()); + + const fetchQueryOpts = { + abortController: expect.any(AbortController), + indexName, + size: expect.any(Number), + startDate: expect.any(String), + endDate: expect.any(String), + }; + + expect(fetchHistoricalResults).toHaveBeenCalledWith( + expect.objectContaining({ + ...fetchQueryOpts, + from: 0, + ...(outcome !== 'All' && { outcome: outcome.toLowerCase() }), + }) + ); + }); + }); + + describe('Super Date Picker', () => { + it('should render superdatepicker with last 30 days preselected', () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + render( + + + + + + + + ); + + const superDatePicker = screen.getByTestId('historicalResultsDatePicker'); + expect(superDatePicker).toBeInTheDocument(); + expect( + within(superDatePicker).getByRole('button', { name: 'Date quick select' }) + ).toBeInTheDocument(); + expect( + within(superDatePicker).getByRole('button', { name: 'Last 30 days' }) + ).toBeInTheDocument(); + expect(within(superDatePicker).getByRole('button', { name: 'Refresh' })).toBeInTheDocument(); + }); + + describe('when new date is selected', () => { + it('should invoke fetchHistoricalResults with new start and end dates, from: 0 and remaining fetch query opts', async () => { + const indexName = 'test'; + const historicalResult = getHistoricalResultStub(indexName); + const fetchHistoricalResults = jest.fn(); + render( + + + + + + + + ); + + const superDatePicker = screen.getByTestId('historicalResultsDatePicker'); + + await act(async () => { + const dateQuickSelect = within(superDatePicker).getByRole('button', { + name: 'Date quick select', + }); + await userEvent.click(dateQuickSelect); + }); + + await act(async () => { + const monthToDateButton = screen.getByRole('button', { name: 'Month to date' }); + + await userEvent.click(monthToDateButton); + }); + + const fetchQueryOpts = { + abortController: expect.any(AbortController), + indexName, + size: expect.any(Number), + }; + + expect(fetchHistoricalResults).toHaveBeenCalledWith( + expect.objectContaining({ + ...fetchQueryOpts, + from: 0, + startDate: 'now/M', + endDate: 'now', + }) + ); + }); + }); + }); + + describe('Pagination', () => { + describe('by default', () => { + it('should show rows per page: 10 by default', () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 20); + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('historicalResultsPagination'); + + expect(within(wrapper).getByText('Rows per page: 10')).toBeInTheDocument(); + }); + }); + + describe('when rows per page are clicked', () => { + it('should show 10, 25, 50 rows per page options', async () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 20); + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('historicalResultsPagination'); + + await act(async () => userEvent.click(within(wrapper).getByText('Rows per page: 10'))); + + expect(screen.getByText('10 rows')).toBeInTheDocument(); + expect(screen.getByText('25 rows')).toBeInTheDocument(); + expect(screen.getByText('50 rows')).toBeInTheDocument(); + }); + }); + + describe('when total results are more than or equal 1 page', () => { + it('should render pagination', () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 20); + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('historicalResultsPagination'); + + expect(within(wrapper).getByText('Rows per page: 10')).toBeInTheDocument(); + expect(within(wrapper).getByRole('list')).toBeInTheDocument(); + }); + }); + + describe('when total results are less than 1 page', () => { + it('should not render pagination', () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 9); + render( + + + + + + + + ); + + expect(screen.queryByTestId('historicalResultsPagination')).not.toBeInTheDocument(); + }); + }); + + describe('when new page is clicked', () => { + it('should invoke fetchHistoricalResults with new from and remaining fetch query opts', async () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 20); + const fetchHistoricalResults = jest.fn(); + render( + + + + + + + + ); + + const nextPageButton = screen.getByLabelText('Page 2 of 2'); + expect(nextPageButton).toHaveRole('button'); + await act(async () => nextPageButton.click()); + + const fetchQueryOpts = { + abortController: expect.any(AbortController), + indexName, + size: expect.any(Number), + startDate: expect.any(String), + endDate: expect.any(String), + }; + + expect(fetchHistoricalResults).toHaveBeenCalledWith( + expect.objectContaining({ + ...fetchQueryOpts, + from: 10, + }) + ); + }); + }); + + describe('when items per page is changed', () => { + it('should invoke fetchHistoricalResults with new size, from: 0 and remaining fetch query opts', async () => { + const indexName = 'test'; + const results = generateHistoricalResultsStub(indexName, 20); + const fetchHistoricalResults = jest.fn(); + render( + + + + + + + + ); + + const wrapper = screen.getByTestId('historicalResultsPagination'); + + await act(async () => userEvent.click(within(wrapper).getByText('Rows per page: 10'))); + + await act(async () => userEvent.click(screen.getByText('25 rows'))); + + const fetchQueryOpts = { + abortController: expect.any(AbortController), + indexName, + from: 0, + startDate: expect.any(String), + endDate: expect.any(String), + }; + + expect(fetchHistoricalResults).toHaveBeenCalledWith( + expect.objectContaining({ + ...fetchQueryOpts, + size: 25, + }) + ); + }); + }); + }); + + describe('when historical results are loading', () => { + it('should show loading screen', () => { + const indexName = 'test'; + render( + + + + + + + + ); + + expect(screen.getByText(LOADING_HISTORICAL_RESULTS)).toBeInTheDocument(); + expect(screen.queryByTestId('historicalResults')).not.toBeInTheDocument(); + }); + }); + + describe('when historical results return error', () => { + it('should show error message', () => { + const indexName = 'test'; + const errorMessage = new Error('An error occurred'); + render( + + + + + + + + ); + + expect(screen.getByText(ERROR_LOADING_HISTORICAL_RESULTS)).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.tsx new file mode 100644 index 0000000000000..66fc6b100de13 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/index.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSuperDatePicker, + EuiTablePagination, +} from '@elastic/eui'; +import React, { FC, useMemo, useReducer } from 'react'; +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { useHistoricalResultsContext } from '../../contexts/historical_results_context'; +import { + DEFAULT_HISTORICAL_RESULTS_END_DATE, + DEFAULT_HISTORICAL_RESULTS_START_DATE, +} from '../constants'; +import { fetchHistoricalResultsQueryReducer } from './reducers/fetch_historical_results_query_reducer'; +import { FetchHistoricalResultsQueryState } from '../types'; +import { LoadingEmptyPrompt } from '../../loading_empty_prompt'; +import { ErrorEmptyPrompt } from '../../error_empty_prompt'; +import { StyledFilterGroupFlexItem, StyledText } from './styles'; +import { + ALL, + ERROR_LOADING_HISTORICAL_RESULTS, + FILTER_RESULTS_BY_OUTCOME, + LOADING_HISTORICAL_RESULTS, + TOTAL_CHECKS, +} from './translations'; +import { DEFAULT_HISTORICAL_RESULTS_PAGE_SIZE } from './constants'; +import { HistoricalResultsList } from './historical_results_list'; +import { useHistoricalResultsPagination } from './hooks/use_historical_results_pagination'; +import { FAIL, PASS } from '../../translations'; +import { useHistoricalResultsOutcomeFilter } from './hooks/use_historical_results_outcome_filter'; +import { useHistoricalResultsDatePicker } from './hooks/use_historical_results_date_picker'; + +const historicalResultsListId = 'historicalResultsList'; + +export const initialFetchHistoricalResultsQueryState: FetchHistoricalResultsQueryState = { + startDate: DEFAULT_HISTORICAL_RESULTS_START_DATE, + endDate: DEFAULT_HISTORICAL_RESULTS_END_DATE, + size: DEFAULT_HISTORICAL_RESULTS_PAGE_SIZE, + from: 0, +}; + +const itemsPerPageOptions = [10, 25, 50]; + +export interface Props { + indexName: string; +} + +export const HistoricalResultsComponent: FC = ({ indexName }) => { + const { formatNumber } = useDataQualityContext(); + + // Manages state for the fetch historical results query object + // used by the fetchHistoricalResults function + const [fetchHistoricalResultsQueryState, fetchHistoricalResultsQueryDispatch] = useReducer( + fetchHistoricalResultsQueryReducer, + initialFetchHistoricalResultsQueryState + ); + + const { paginationState, handleChangeActivePage, handleChangeItemsPerPage } = + useHistoricalResultsPagination({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, + }); + + const { + handleDefaultOutcome, + handlePassOutcome, + handleFailOutcome, + isShowAll, + isShowPass, + isShowFail, + } = useHistoricalResultsOutcomeFilter({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, + }); + + const { handleTimeChange } = useHistoricalResultsDatePicker({ + indexName, + fetchHistoricalResultsQueryState, + fetchHistoricalResultsQueryDispatch, + }); + + const { historicalResultsState } = useHistoricalResultsContext(); + + const totalResultsFormatted = useMemo( + () => formatNumber(historicalResultsState.total), + [formatNumber, historicalResultsState.total] + ); + + if (historicalResultsState.isLoading) { + return ; + } + + if (historicalResultsState.error) { + return ; + } + + const totalChecksText = TOTAL_CHECKS(historicalResultsState.total, totalResultsFormatted); + + return ( +
+ + + + + {ALL} + + + {PASS} + + + {FAIL} + + + + + + + + + + {totalChecksText} + +
+ +
+ {paginationState.pageCount > 1 ? ( +
+ + +
+ ) : null} +
+ ); +}; + +HistoricalResultsComponent.displayName = 'HistoricalResultsComponent'; + +export const HistoricalResults = React.memo(HistoricalResultsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.test.ts new file mode 100644 index 0000000000000..5392acc98da87 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { initialFetchHistoricalResultsQueryState } from '..'; +import { fetchHistoricalResultsQueryReducer } from './fetch_historical_results_query_reducer'; + +describe('fetchHistoricalResultsQueryReducer', () => { + describe('on SET_DATE', () => { + it('should set startDate and endDate and reset from to 0', () => { + const state = { + ...initialFetchHistoricalResultsQueryState, + } as const; + const action = { + type: 'SET_DATE', + payload: { startDate: '2021-02-01', endDate: '2021-02-28' }, + } as const; + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual({ + ...initialFetchHistoricalResultsQueryState, + startDate: '2021-02-01', + endDate: '2021-02-28', + from: 0, + }); + }); + }); + + describe('on SET_OUTCOME', () => { + it('should set outcome and reset from to 0', () => { + const state = { + ...initialFetchHistoricalResultsQueryState, + outcome: 'pass', + from: 10, + } as const; + const action = { type: 'SET_OUTCOME', payload: 'fail' } as const; + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual({ + ...initialFetchHistoricalResultsQueryState, + outcome: 'fail', + from: 0, + }); + }); + + it('should omit outcome from the query', () => { + const state = { + ...initialFetchHistoricalResultsQueryState, + outcome: 'pass', + from: 10, + } as const; + const action = { type: 'SET_OUTCOME' as const, payload: undefined } as const; + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual({ ...initialFetchHistoricalResultsQueryState, from: 0 }); + }); + }); + + describe('on SET_FROM', () => { + it('should set from', () => { + const state = { ...initialFetchHistoricalResultsQueryState, from: 10 } as const; + const action = { type: 'SET_FROM' as const, payload: 20 } as const; + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual({ ...initialFetchHistoricalResultsQueryState, from: 20 }); + }); + }); + + describe('on SET_SIZE', () => { + it('should set size and reset from to 0', () => { + const state = { ...initialFetchHistoricalResultsQueryState, size: 10, from: 10 } as const; + const action = { type: 'SET_SIZE' as const, payload: 20 } as const; + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual({ ...initialFetchHistoricalResultsQueryState, size: 20, from: 0 }); + }); + }); + + describe('on unknown action', () => { + it('should return the state', () => { + const state = { ...initialFetchHistoricalResultsQueryState, size: 10, from: 10 } as const; + const action = { type: 'UNKNOWN_ACTION' }; + // @ts-expect-error + const newState = fetchHistoricalResultsQueryReducer(state, action); + expect(newState).toEqual(state); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.ts new file mode 100644 index 0000000000000..c187fdba48a88 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/reducers/fetch_historical_results_query_reducer.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FetchHistoricalResultsQueryState } from '../../types'; +import { FetchHistoricalResultsQueryAction } from '../types'; + +export const fetchHistoricalResultsQueryReducer = ( + state: FetchHistoricalResultsQueryState, + action: FetchHistoricalResultsQueryAction +) => { + if (action.type === 'SET_DATE') { + return { + ...state, + startDate: action.payload.startDate, + endDate: action.payload.endDate, + from: 0, + }; + } + + if (action.type === 'SET_OUTCOME') { + if (action.payload === undefined) { + // omit outcome from the query + const { outcome, ...rest } = state; + return { + ...rest, + from: 0, + }; + } + + return { + ...state, + outcome: action.payload, + from: 0, + }; + } + + if (action.type === 'SET_FROM') { + return { + ...state, + from: action.payload, + }; + } + + if (action.type === 'SET_SIZE') { + return { + ...state, + size: action.payload, + from: 0, + }; + } + + return state; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/styles.tsx new file mode 100644 index 0000000000000..5c8baef905ec2 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/styles.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; + +export const StyledFilterGroupFlexItem = styled(EuiFlexItem)` + flex-basis: 17%; +`; + +export const StyledText = styled(EuiText)` + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/translations.ts new file mode 100644 index 0000000000000..c0449ab0c7a62 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOADING_HISTORICAL_RESULTS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.loadingHistoricalResults', + { + defaultMessage: 'Loading historical results', + } +); + +export const ERROR_LOADING_HISTORICAL_RESULTS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.errorLoadingHistoricalResults', + { + defaultMessage: 'Unable to load historical results', + } +); + +export const TOTAL_CHECKS = (count: number, formattedCount: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.totalChecks', { + values: { + count, + formattedCount, + }, + defaultMessage: '{formattedCount} {count, plural, one {check} other {checks}}', + }); + +export const FILTER_RESULTS_BY_OUTCOME = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.filterResultsByOutcome', + { + defaultMessage: 'Filter results by outcome', + } +); + +export const ALL = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.all', { + defaultMessage: 'All', +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/types.ts new file mode 100644 index 0000000000000..053df2283a680 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/historical_results/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type FetchHistoricalResultsQueryAction = + | { type: 'SET_DATE'; payload: { startDate: string; endDate: string } } + | { type: 'SET_OUTCOME'; payload: 'pass' | 'fail' | undefined } + | { type: 'SET_FROM'; payload: number } + | { type: 'SET_SIZE'; payload: number }; + +export type PaginationReducerAction = + | { type: 'SET_ROW_SIZE'; payload: { rowSize: number; totalResults: number } } + | { type: 'SET_ACTIVE_PAGE'; payload: number }; + +export interface PaginationReducerState { + rowSize: number; + pageCount: number; + activePage: number; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.test.tsx new file mode 100644 index 0000000000000..2f0e5a3774e16 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsVersion } from '@elastic/ecs'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; +import { IncompatibleCallout } from '.'; +import { + DETECTION_ENGINE_RULES_MAY_NOT_MATCH, + MAPPINGS_THAT_CONFLICT_WITH_ECS, + PAGES_MAY_NOT_DISPLAY_EVENTS, +} from '../../../../../translations'; + +describe('IncompatibleCallout', () => { + it('should warn rules may not match', () => { + render( + + + + ); + expect(screen.getByTestId('rulesMayNotMatch')).toHaveTextContent( + DETECTION_ENGINE_RULES_MAY_NOT_MATCH + ); + }); + + it('should warn pages may not display events', () => { + render( + + + + ); + expect(screen.getByTestId('pagesMayNotDisplayEvents')).toHaveTextContent( + PAGES_MAY_NOT_DISPLAY_EVENTS + ); + }); + + it("should warn mappings that don't comply with ECS are unsupported", () => { + render( + + + + ); + expect(screen.getByTestId('mappingsThatDontComply')).toHaveTextContent( + MAPPINGS_THAT_CONFLICT_WITH_ECS + ); + }); + + describe('given an incompatible field count', () => { + it('should include the count in the title', () => { + render( + + + + ); + expect(screen.getByTestId('incompatibleCallout')).toHaveTextContent('3 incompatible fields'); + }); + }); + + describe('given no incompatible field count', () => { + it('should not include the count in the title', () => { + render( + + + + ); + expect(screen.getByTestId('incompatibleCallout')).not.toHaveTextContent( + '3 incompatible fields' + ); + }); + }); + + describe('given an ECS version', () => { + it('should include the provided ECS version in the main content', () => { + render( + + + + ); + expect(screen.getByTestId('fieldsAreIncompatible')).toHaveTextContent( + `Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 1.8.0.` + ); + }); + }); + + describe('given no ECS version', () => { + it('should include the default ECS version in the main content', () => { + render( + + + + ); + expect(screen.getByTestId('fieldsAreIncompatible')).toHaveTextContent( + `Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.` + ); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.tsx new file mode 100644 index 0000000000000..e0a647872edfc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_callout/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsVersion } from '@elastic/ecs'; + +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import React from 'react'; + +import { + DETECTION_ENGINE_RULES_MAY_NOT_MATCH, + INCOMPATIBLE_CALLOUT, + INCOMPATIBLE_CALLOUT_TITLE, + MAPPINGS_THAT_CONFLICT_WITH_ECS, + PAGES_MAY_NOT_DISPLAY_EVENTS, +} from '../../../../../translations'; +import { CalloutItem } from '../styles'; + +export interface Props { + incompatibleFieldCount?: number; + ecsVersion?: string; +} + +const IncompatibleCalloutComponent: React.FC = ({ + ecsVersion = EcsVersion, + incompatibleFieldCount, +}) => { + return ( + +
{INCOMPATIBLE_CALLOUT(ecsVersion)}
+ + + {DETECTION_ENGINE_RULES_MAY_NOT_MATCH} + + + {PAGES_MAY_NOT_DISPLAY_EVENTS} + + + {MAPPINGS_THAT_CONFLICT_WITH_ECS} + +
+ ); +}; + +IncompatibleCalloutComponent.displayName = 'IncompatibleCalloutComponent'; + +export const IncompatibleCallout = React.memo(IncompatibleCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/index.tsx new file mode 100644 index 0000000000000..83cf36b983e14 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { Actions } from '../../../../../actions'; +import { getAllIncompatibleMarkdownComments } from '../../../../../utils/markdown'; +import { IncompatibleCallout } from '../incompatible_callout'; +import { CompareFieldsTable } from '../compare_fields_table'; +import { + getIncompatibleMappingsTableColumns, + getIncompatibleValuesTableColumns, +} from './utils/get_incompatible_table_columns'; +import type { IlmPhase, IncompatibleFieldMetadata } from '../../../../../types'; +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { StickyActions } from '../latest_results/latest_check_fields/sticky_actions'; +import { + INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, + INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE, +} from '../../../../../translations'; +import { CheckSuccessEmptyPrompt } from '../check_success_empty_prompt'; + +interface Props { + docsCount: number; + ilmPhase: IlmPhase | undefined; + indexName: string; + patternDocsCount?: number; + sizeInBytes: number | undefined; + incompatibleMappingsFields: IncompatibleFieldMetadata[]; + incompatibleValuesFields: IncompatibleFieldMetadata[]; + sameFamilyFieldsCount: number; + ecsCompliantFieldsCount: number; + customFieldsCount: number; + allFieldsCount: number; + hasStickyActions?: boolean; +} + +const IncompatibleTabComponent: React.FC = ({ + docsCount, + ilmPhase, + indexName, + patternDocsCount, + sizeInBytes, + incompatibleMappingsFields, + incompatibleValuesFields, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + customFieldsCount, + allFieldsCount, + hasStickyActions = true, +}) => { + const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); + + const incompatibleFieldCount = + incompatibleMappingsFields.length + incompatibleValuesFields.length; + + const markdownComment: string = useMemo( + () => + getAllIncompatibleMarkdownComments({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleMappingsFields, + incompatibleValuesFields, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + customFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, + }).join('\n'), + [ + allFieldsCount, + customFieldsCount, + docsCount, + ecsCompliantFieldsCount, + formatBytes, + formatNumber, + ilmPhase, + incompatibleMappingsFields, + incompatibleValuesFields, + indexName, + isILMAvailable, + patternDocsCount, + sameFamilyFieldsCount, + sizeInBytes, + ] + ); + + return ( +
+ {incompatibleFieldCount > 0 ? ( + <> + + + <> + {incompatibleMappingsFields.length > 0 && ( + <> + + + + + )} + + + <> + {incompatibleValuesFields.length > 0 && ( + <> + + + + + )} + + + + {hasStickyActions ? ( + + ) : ( + + )} + + ) : ( + + )} +
+ ); +}; + +IncompatibleTabComponent.displayName = 'IncompatibleTabComponent'; + +export const IncompatibleTab = React.memo(IncompatibleTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.test.tsx new file mode 100644 index 0000000000000..c7a24b5830ddc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.test.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { omit } from 'lodash/fp'; +import { render, screen } from '@testing-library/react'; + +import { + getIncompatibleMappingsTableColumns, + getIncompatibleValuesTableColumns, +} from './get_incompatible_table_columns'; +import { TestExternalProviders } from '../../../../../../mock/test_providers/test_providers'; +import { + eventCategoryWithUnallowedValues, + hostNameWithTextMapping, +} from '../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; + +describe('getIncompatibleMappingsTableColumns', () => { + test('it returns the expected column configuration', () => { + const columns = getIncompatibleMappingsTableColumns().map((x) => omit('render', x)); + + expect(columns).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: 'ECS mapping type (expected)', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'indexFieldType', + name: 'Index mapping type (actual)', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '35%', + }, + ]); + }); + + describe('type column render()', () => { + test('it renders the expected type', () => { + const columns = getIncompatibleMappingsTableColumns(); + const typeColumnRender = columns[1].render; + const expected = 'keyword'; + + render( + + {typeColumnRender != null && + typeColumnRender(hostNameWithTextMapping.type, hostNameWithTextMapping)} + + ); + + expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected); + }); + }); + + describe('indexFieldType column render()', () => { + const indexFieldType = 'text'; + + test('it renders the expected type with danger styling', () => { + const columns = getIncompatibleMappingsTableColumns(); + const indexFieldTypeColumnRender = columns[2].render; + + render( + + {indexFieldTypeColumnRender != null && + indexFieldTypeColumnRender( + hostNameWithTextMapping.indexFieldType, + hostNameWithTextMapping + )} + + ); + + expect(screen.getByTestId('codeDanger')).toHaveTextContent(indexFieldType); + }); + }); +}); + +describe('getIncompatibleValuesTableColumns', () => { + test('it returns the expected columns', () => { + expect(getIncompatibleValuesTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'allowed_values', + name: 'ECS values (expected)', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'indexInvalidValues', + name: 'Document values (actual)', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '35%', + }, + ]); + }); + + describe('allowed values render()', () => { + describe('when `allowedValues` exists', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const allowedValuesRender = columns[1].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender( + eventCategoryWithUnallowedValues.allowed_values, + eventCategoryWithUnallowedValues + )} + + + ); + }); + + test('it renders the expected `AllowedValue` names', () => { + expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( + eventCategoryWithUnallowedValues.allowed_values?.map(({ name }) => name).join('') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); + }); + }); + + describe('when `allowedValues` is undefined', () => { + const withUndefinedAllowedValues = { + ...eventCategoryWithUnallowedValues, + allowed_values: undefined, // <-- + }; + + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const allowedValuesRender = columns[1].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender( + withUndefinedAllowedValues.allowed_values, + withUndefinedAllowedValues + )} + + + ); + }); + + test('it does NOT render the `AllowedValue` names', () => { + expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); + }); + }); + }); + + describe('indexInvalidValues render()', () => { + describe('when `indexInvalidValues` is populated', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const indexInvalidValuesRender = columns[2].render; + + render( + + <> + {indexInvalidValuesRender != null && + indexInvalidValuesRender( + eventCategoryWithUnallowedValues.indexInvalidValues, + eventCategoryWithUnallowedValues + )} + + + ); + }); + + test('it renders the expected `indexInvalidValues`', () => { + expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( + 'an_invalid_category (2)theory (1)' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); + }); + }); + + describe('when `indexInvalidValues` is empty', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const indexInvalidValuesRender = columns[2].render; + + const withEmptyIndexInvalidValues = { + ...eventCategoryWithUnallowedValues, + indexInvalidValues: [], // <-- + }; + + render( + + <> + {indexInvalidValuesRender != null && + indexInvalidValuesRender( + withEmptyIndexInvalidValues.indexInvalidValues, + withEmptyIndexInvalidValues + )} + + + ); + }); + + test('it does NOT render the index invalid values', () => { + expect(screen.queryByTestId('indexInvalidValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.tsx new file mode 100644 index 0000000000000..c39b29d0a6597 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/incompatible_tab/utils/get_incompatible_table_columns.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTableFieldDataColumnType } from '@elastic/eui'; + +import { CodeDanger, CodeSuccess } from '../../../../../../styles'; +import { + AllowedValue, + IncompatibleFieldMetadata, + UnallowedValueCount, +} from '../../../../../../types'; +import { + DOCUMENT_VALUES_ACTUAL, + ECS_MAPPING_TYPE_EXPECTED, + ECS_VALUES_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, +} from '../../../../../../translations'; +import { EcsAllowedValues } from '../../ecs_allowed_values'; +import { IndexInvalidValues } from '../../index_invalid_values'; +import { ECS_DESCRIPTION } from '../../translations'; + +export const getIncompatibleValuesTableColumns = (): Array< + EuiTableFieldDataColumnType +> => [ + { + field: 'indexFieldName', + name: FIELD, + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'allowed_values', + name: ECS_VALUES_EXPECTED, + render: (allowedValues: AllowedValue[] | undefined) => ( + + ), + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'indexInvalidValues', + name: DOCUMENT_VALUES_ACTUAL, + render: (indexInvalidValues: UnallowedValueCount[]) => ( + + ), + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: ECS_DESCRIPTION, + sortable: false, + truncateText: false, + width: '35%', + }, +]; + +export const getIncompatibleMappingsTableColumns = (): Array< + EuiTableFieldDataColumnType +> => [ + { + field: 'indexFieldName', + name: FIELD, + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: ECS_MAPPING_TYPE_EXPECTED, + render: (type: string) => {type}, + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'indexFieldType', + name: INDEX_MAPPING_TYPE_ACTUAL, + render: (indexFieldType: string, x) => ( + {indexFieldType} + ), + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: ECS_DESCRIPTION, + sortable: false, + truncateText: false, + width: '35%', + }, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx index 83b74daae365a..7b63f712a99da 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx @@ -7,21 +7,24 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import moment from 'moment'; import userEvent from '@testing-library/user-event'; import { IndexCheckFlyout } from '.'; import { TestDataQualityProviders, TestExternalProviders, + TestHistoricalResultsProvider, } from '../../../../mock/test_providers/test_providers'; import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { mockStats } from '../../../../mock/stats/mock_stats'; +import { mockHistoricalResult } from '../../../../mock/historical_results/mock_historical_results_response'; +import { getFormattedCheckTime } from './utils/get_formatted_check_time'; describe('IndexCheckFlyout', () => { beforeEach(() => { jest.clearAllMocks(); + jest.restoreAllMocks(); }); describe('rendering', () => { @@ -29,14 +32,17 @@ describe('IndexCheckFlyout', () => { render( - + + + ); @@ -51,15 +57,24 @@ describe('IndexCheckFlyout', () => { 'auditbeat-custom-index-1' ); expect(screen.getByTestId('latestCheckedAt')).toHaveTextContent( - moment(auditbeatWithAllResults.results!['auditbeat-custom-index-1'].checkedAt).format( - 'MMM DD, YYYY @ HH:mm:ss.SSS' + getFormattedCheckTime( + auditbeatWithAllResults.results!['auditbeat-custom-index-1'].checkedAt! ) ); }); + it('should render tabs correctly, with latest check preselected', () => { + expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect(screen.getByRole('tab', { name: 'Latest Check' })).not.toBeDisabled(); + expect(screen.getByRole('tab', { name: 'History' })).not.toBeDisabled(); + }); + it('should render the correct index properties panel', () => { expect(screen.getByTestId('indexStatsPanel')).toBeInTheDocument(); - expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument(); + expect(screen.getByTestId('latestCheckFields')).toBeInTheDocument(); }); it('should render footer with check now button', () => { @@ -73,14 +88,17 @@ describe('IndexCheckFlyout', () => { render( - + + + ); @@ -102,14 +120,17 @@ describe('IndexCheckFlyout', () => { checkIndex, }} > - + + +
); @@ -127,4 +148,63 @@ describe('IndexCheckFlyout', () => { }); }); }); + + describe('when history tab is clicked', () => { + it('should call fetchHistoricalResults and switch to history tab', async () => { + const fetchHistoricalResults = jest.fn(); + + const historicalResultsState = { + results: [mockHistoricalResult], + total: 1, + isLoading: false, + error: null, + }; + + render( + + + + + + + + ); + + expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect(screen.getByRole('tab', { name: 'History' })).not.toHaveAttribute( + 'aria-selected', + 'true' + ); + + const historyTab = screen.getByRole('tab', { name: 'History' }); + await userEvent.click(historyTab); + + expect(fetchHistoricalResults).toHaveBeenCalledWith({ + indexName: 'auditbeat-custom-index-1', + abortController: expect.any(AbortController), + }); + + expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Latest Check' })).not.toHaveAttribute( + 'aria-selected', + 'true' + ); + + expect(screen.getByTestId('historicalResults')).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx index 0ae749a216856..f298af704307d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx @@ -15,24 +15,29 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiSpacer, + EuiTab, + EuiTabs, EuiText, EuiTitle, useGeneratedHtmlId, } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import moment from 'moment'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { getIlmPhase } from '../../../../utils/get_ilm_phase'; -import { getDocsCount, getSizeInBytes } from '../../../../utils/stats'; +import { useAbortControllerRef } from '../../../../hooks/use_abort_controller_ref'; import { useIndicesCheckContext } from '../../../../contexts/indices_check_context'; -import { EMPTY_STAT } from '../../../../constants'; import { MeteringStatsIndex, PatternRollup } from '../../../../types'; import { useDataQualityContext } from '../../../../data_quality_context'; -import { IndexProperties } from './index_properties'; import { IndexResultBadge } from '../index_result_badge'; import { useCurrentWindowWidth } from './hooks/use_current_window_width'; -import { CHECK_NOW } from './translations'; +import { HISTORY, LATEST_CHECK } from './translations'; +import { LatestResults } from './latest_results'; +import { HistoricalResults } from './historical_results'; +import { useHistoricalResultsContext } from '../contexts/historical_results_context'; +import { getFormattedCheckTime } from './utils/get_formatted_check_time'; +import { CHECK_NOW } from '../translations'; +import { HISTORY_TAB_ID, LATEST_CHECK_TAB_ID } from '../constants'; +import { IndexCheckFlyoutTabId } from './types'; export interface Props { ilmExplain: Record | null; @@ -41,20 +46,35 @@ export interface Props { patternRollup: PatternRollup | undefined; stats: Record | null; onClose: () => void; + initialSelectedTabId: IndexCheckFlyoutTabId; } +const tabs = [ + { + id: LATEST_CHECK_TAB_ID, + name: LATEST_CHECK, + }, + { + id: HISTORY_TAB_ID, + name: HISTORY, + }, +] as const; + export const IndexCheckFlyoutComponent: React.FC = ({ ilmExplain, indexName, + initialSelectedTabId, pattern, patternRollup, stats, onClose, }) => { + const didSwitchToLatestTabOnceRef = useRef(false); + const { fetchHistoricalResults } = useHistoricalResultsContext(); const currentWindowWidth = useCurrentWindowWidth(); const isLargeScreen = currentWindowWidth > 1720; const isMediumScreen = currentWindowWidth > 1200; - const { httpFetch, formatBytes, formatNumber, isILMAvailable } = useDataQualityContext(); + const { httpFetch, formatBytes, formatNumber } = useDataQualityContext(); const { checkState, checkIndex } = useIndicesCheckContext(); const indexCheckState = checkState[indexName]; const isChecking = indexCheckState?.isChecking ?? false; @@ -63,25 +83,87 @@ export const IndexCheckFlyoutComponent: React.FC = ({ const indexCheckFlyoutTitleId = useGeneratedHtmlId({ prefix: 'indexCheckFlyoutTitle', }); - const abortControllerRef = React.useRef(new AbortController()); + const [selectedTabId, setSelectedTabId] = useState(initialSelectedTabId); + const checkNowButtonAbortControllerRef = useAbortControllerRef(); + const checkLatestTabAbortControllerRef = useAbortControllerRef(); + const fetchHistoricalResultsAbortControllerRef = useAbortControllerRef(); + + const handleTabClick = useCallback( + (tabId: IndexCheckFlyoutTabId) => { + if (tabId === HISTORY_TAB_ID) { + fetchHistoricalResults({ + abortController: fetchHistoricalResultsAbortControllerRef.current, + indexName, + }); + setSelectedTabId(tabId); + } + + if (tabId === LATEST_CHECK_TAB_ID) { + if (!didSwitchToLatestTabOnceRef.current) { + didSwitchToLatestTabOnceRef.current = true; + checkIndex({ + abortController: checkLatestTabAbortControllerRef.current, + indexName, + pattern, + httpFetch, + formatBytes, + formatNumber, + }); + } + setSelectedTabId(tabId); + } + }, + [ + checkIndex, + checkLatestTabAbortControllerRef, + fetchHistoricalResults, + fetchHistoricalResultsAbortControllerRef, + formatBytes, + formatNumber, + httpFetch, + indexName, + pattern, + ] + ); const handleCheckNow = useCallback(() => { checkIndex({ - abortController: abortControllerRef.current, + abortController: checkNowButtonAbortControllerRef.current, indexName, pattern, httpFetch, formatBytes, formatNumber, }); - }, [checkIndex, formatBytes, formatNumber, httpFetch, indexName, pattern]); + if (selectedTabId === HISTORY_TAB_ID) { + setSelectedTabId(LATEST_CHECK_TAB_ID); + } + }, [ + checkIndex, + checkNowButtonAbortControllerRef, + formatBytes, + formatNumber, + httpFetch, + indexName, + pattern, + selectedTabId, + ]); - useEffect(() => { - const abortController = abortControllerRef.current; - return () => { - abortController.abort(); - }; - }, []); + const renderTabs = useMemo( + () => + tabs.map((tab, index) => { + return ( + handleTabClick(tab.id)} + isSelected={tab.id === selectedTabId} + key={index} + > + {tab.name} + + ); + }), + [handleTabClick, selectedTabId] + ); return (
@@ -104,26 +186,24 @@ export const IndexCheckFlyoutComponent: React.FC = ({ <> - {moment(indexResult.checkedAt).isValid() - ? moment(indexResult.checkedAt).format('MMM DD, YYYY @ HH:mm:ss.SSS') - : EMPTY_STAT} + {getFormattedCheckTime(indexResult.checkedAt)} )} + + {renderTabs} - + {selectedTabId === LATEST_CHECK_TAB_ID ? ( + + ) : ( + + )} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.test.tsx similarity index 85% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.test.tsx index 8a53f4cdaf546..daa491bf065b7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.test.tsx @@ -8,10 +8,10 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { EMPTY_PLACEHOLDER } from '../helpers'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; -import { UnallowedValueCount } from '../../../../../../../../../types'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; +import { UnallowedValueCount } from '../../../../../types'; import { IndexInvalidValues } from '.'; +import { EMPTY_PLACEHOLDER } from '../../../../../constants'; describe('IndexInvalidValues', () => { test('it renders a placeholder with the expected content when `indexInvalidValues` is empty', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.tsx similarity index 88% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.tsx index 7f83876423ba9..812e369d4039f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/index_invalid_values/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_invalid_values/index.tsx @@ -9,9 +9,9 @@ import { EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { EMPTY_PLACEHOLDER } from '../helpers'; -import { CodeDanger } from '../../../../../../../../../styles'; -import type { UnallowedValueCount } from '../../../../../../../../../types'; +import { EMPTY_PLACEHOLDER } from '../../../../../constants'; +import { CodeDanger } from '../../../../../styles'; +import type { UnallowedValueCount } from '../../../../../types'; const IndexInvalidValueFlexItem = styled(EuiFlexItem)` margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx deleted file mode 100644 index 03d293a02e69a..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiSpacer } from '@elastic/eui'; -import { ErrorEmptyPrompt } from '../../error_empty_prompt'; -import { LoadingEmptyPrompt } from '../../loading_empty_prompt'; -import * as i18n from './translations'; -import type { IlmPhase, PatternRollup } from '../../../../../types'; -import { useIndicesCheckContext } from '../../../../../contexts/indices_check_context'; -import { IndexCheckFields } from './index_check_fields'; -import { IndexStatsPanel } from './index_stats_panel'; -import { useDataQualityContext } from '../../../../../data_quality_context'; -import { getIndexPropertiesContainerId } from './utils/get_index_properties_container_id'; - -export interface Props { - docsCount: number; - ilmPhase: IlmPhase | undefined; - indexName: string; - pattern: string; - patternRollup: PatternRollup | undefined; - sizeInBytes?: number; -} - -const IndexPropertiesComponent: React.FC = ({ - docsCount, - ilmPhase, - indexName, - pattern, - patternRollup, - sizeInBytes, -}) => { - const { checkState } = useIndicesCheckContext(); - const { formatBytes, formatNumber } = useDataQualityContext(); - const indexCheckState = checkState[indexName]; - const isChecking = indexCheckState?.isChecking ?? false; - const isLoadingMappings = indexCheckState?.isLoadingMappings ?? false; - const isLoadingUnallowedValues = indexCheckState?.isLoadingUnallowedValues ?? false; - const genericCheckError = indexCheckState?.genericError ?? null; - const mappingsError = indexCheckState?.mappingsError ?? null; - const unallowedValuesError = indexCheckState?.unallowedValuesError ?? null; - const isCheckComplete = indexCheckState?.isCheckComplete ?? false; - - if (mappingsError != null) { - return ; - } else if (unallowedValuesError != null) { - return ; - } else if (genericCheckError != null) { - return ; - } - - if (isLoadingMappings) { - return ; - } else if (isLoadingUnallowedValues) { - return ; - } else if (isChecking) { - return ; - } - - return isCheckComplete ? ( -
- {ilmPhase && ( - - )} - - -
- ) : null; -}; - -IndexPropertiesComponent.displayName = 'IndexPropertiesComponent'; - -export const IndexProperties = React.memo(IndexPropertiesComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx deleted file mode 100644 index 7e69a906c42ad..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { EuiButtonGroup, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import styled from 'styled-components'; - -import { EMPTY_METADATA } from '../../../../../../constants'; -import { useDataQualityContext } from '../../../../../../data_quality_context'; -import { useIndicesCheckContext } from '../../../../../../contexts/indices_check_context'; -import { INCOMPATIBLE_TAB_ID } from './constants'; -import { IlmPhase, PatternRollup } from '../../../../../../types'; -import { getTabs } from './tabs/helpers'; - -const StyledTabFlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const StyledTabFlexItem = styled.div` - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -`; - -const StyledButtonGroup = styled(EuiButtonGroup)` - button[data-test-subj='incompatibleTab'] { - flex-grow: 1.2; - } - button[data-test-subj='ecsCompliantTab'] { - flex-grow: 1.4; - } -`; - -export interface Props { - docsCount: number; - ilmPhase: IlmPhase | undefined; - indexName: string; - patternRollup: PatternRollup | undefined; -} - -const IndexCheckFieldsComponent: React.FC = ({ - indexName, - ilmPhase, - patternRollup, - docsCount, -}) => { - const { formatBytes, formatNumber } = useDataQualityContext(); - const { checkState } = useIndicesCheckContext(); - const partitionedFieldMetadata = checkState[indexName]?.partitionedFieldMetadata ?? null; - - const [selectedTabId, setSelectedTabId] = useState(INCOMPATIBLE_TAB_ID); - - const tabs = useMemo( - () => - getTabs({ - formatBytes, - formatNumber, - docsCount, - ilmPhase, - indexName, - partitionedFieldMetadata: partitionedFieldMetadata ?? EMPTY_METADATA, - patternDocsCount: patternRollup?.docsCount ?? 0, - stats: patternRollup?.stats ?? null, - }), - [ - formatBytes, - formatNumber, - docsCount, - ilmPhase, - indexName, - partitionedFieldMetadata, - patternRollup?.docsCount, - patternRollup?.stats, - ] - ); - - const tabSelections = tabs.map((tab) => ({ - id: tab.id, - label: ( - - {tab.name} - {tab.append} - - ), - textProps: false as false, - })); - - const handleSelectedTabId = (optionId: string) => { - setSelectedTabId(optionId); - }; - - return ( -
- - - {tabs.find((tab) => tab.id === selectedTabId)?.content} -
- ); -}; - -IndexCheckFieldsComponent.displayName = 'IndexFieldsComponent'; - -export const IndexCheckFields = React.memo(IndexCheckFieldsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.test.tsx deleted file mode 100644 index ffb315c266669..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { - DETECTION_ENGINE_RULES_MAY_NOT_MATCH, - MAPPINGS_THAT_CONFLICT_WITH_ECS, - PAGES_MAY_NOT_DISPLAY_EVENTS, -} from '../../../../translations'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; -import { IncompatibleCallout } from '.'; - -describe('IncompatibleCallout', () => { - beforeEach(() => { - render( - - - - ); - }); - - test('it includes the ECS version in the main content', () => { - expect(screen.getByTestId('fieldsAreIncompatible')).toHaveTextContent( - `Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.` - ); - }); - - test('it warns rules may not match', () => { - expect(screen.getByTestId('rulesMayNotMatch')).toHaveTextContent( - DETECTION_ENGINE_RULES_MAY_NOT_MATCH - ); - }); - - test('it warns pages may not display events', () => { - expect(screen.getByTestId('pagesMayNotDisplayEvents')).toHaveTextContent( - PAGES_MAY_NOT_DISPLAY_EVENTS - ); - }); - - test("it warns mappings that don't comply with ECS are unsupported", () => { - expect(screen.getByTestId('mappingsThatDontComply')).toHaveTextContent( - MAPPINGS_THAT_CONFLICT_WITH_ECS - ); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.tsx deleted file mode 100644 index 41a69eb69424a..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/incompatible_callout/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; - -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../../../../translations'; -import { CalloutItem } from '../../styles'; - -const IncompatibleCalloutComponent: React.FC = () => { - return ( - -
{i18n.INCOMPATIBLE_CALLOUT(EcsVersion)}
- - - {i18n.DETECTION_ENGINE_RULES_MAY_NOT_MATCH} - - - {i18n.PAGES_MAY_NOT_DISPLAY_EVENTS} - - - {i18n.MAPPINGS_THAT_CONFLICT_WITH_ECS} - -
- ); -}; - -IncompatibleCalloutComponent.displayName = 'IncompatibleCalloutComponent'; - -export const IncompatibleCallout = React.memo(IncompatibleCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/missing_timestamp_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/missing_timestamp_callout/index.tsx deleted file mode 100644 index 3e9b1f705d1eb..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/missing_timestamp_callout/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../../../../translations'; -import { CalloutItem } from '../../styles'; - -interface Props { - children?: React.ReactNode; -} - -const MissingTimestampCalloutComponent: React.FC = ({ children }) => ( - -
{i18n.MISSING_TIMESTAMP_CALLOUT}
- - {i18n.DETECTION_ENGINE_RULES_MAY_NOT_MATCH} - {i18n.PAGES_MAY_NOT_DISPLAY_EVENTS} - - {children} -
-); - -MissingTimestampCalloutComponent.displayName = 'MissingTimestampCalloutComponent'; - -export const MissingTimestampCallout = React.memo(MissingTimestampCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx deleted file mode 100644 index a1d37950da554..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, screen } from '@testing-library/react'; -import { omit } from 'lodash/fp'; -import React from 'react'; - -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; -import { eventCategory } from '../../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { EcsBasedFieldMetadata } from '../../../../../../../../../types'; -import { getIncompatibleMappingsTableColumns } from '.'; -import { SAME_FAMILY_BADGE_LABEL } from '../../../translate'; - -describe('getIncompatibleMappingsTableColumns', () => { - test('it returns the expected column configuration', () => { - const columns = getIncompatibleMappingsTableColumns().map((x) => omit('render', x)); - - expect(columns).toEqual([ - { - field: 'indexFieldName', - name: 'Field', - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'type', - name: 'ECS mapping type (expected)', - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'indexFieldType', - name: 'Index mapping type (actual)', - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: 'ECS description', - sortable: false, - truncateText: false, - width: '35%', - }, - ]); - }); - - describe('type column render()', () => { - test('it renders the expected type', () => { - const columns = getIncompatibleMappingsTableColumns(); - const typeColumnRender = columns[1].render; - const expected = 'keyword'; - - render( - - {typeColumnRender != null && typeColumnRender(eventCategory.type, eventCategory)} - - ); - - expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected); - }); - }); - - describe('indexFieldType column render()', () => { - describe("when the index field type does NOT match the ECS type, but it's in the SAME family", () => { - const indexFieldType = 'wildcard'; - - beforeEach(() => { - const columns = getIncompatibleMappingsTableColumns(); - const indexFieldTypeColumnRender = columns[2].render; - - const withTypeMismatchSameFamily: EcsBasedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType, // this index has a mapping of `wildcard` instead of `keyword` - isInSameFamily: true, // `wildcard` and `keyword` are in the same family - }; - - render( - - {indexFieldTypeColumnRender != null && - indexFieldTypeColumnRender( - withTypeMismatchSameFamily.indexFieldType, - withTypeMismatchSameFamily - )} - - ); - }); - - test('it renders the expected type with a "success" style', () => { - expect(screen.getByTestId('codeSuccess')).toHaveTextContent(indexFieldType); - }); - - test('it renders the same family badge', () => { - expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY_BADGE_LABEL); - }); - }); - - describe("when the index field type does NOT match the ECS type, but it's in a DIFFERENT family", () => { - const indexFieldType = 'text'; - - test('it renders the expected type with danger styling', () => { - const columns = getIncompatibleMappingsTableColumns(); - const indexFieldTypeColumnRender = columns[2].render; - - const withTypeMismatchDifferentFamily: EcsBasedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType, // this index has a mapping of `text` instead of `keyword` - isInSameFamily: false, // `text` and `wildcard` are not in the same family - }; - - render( - - {indexFieldTypeColumnRender != null && - indexFieldTypeColumnRender( - withTypeMismatchDifferentFamily.indexFieldType, - withTypeMismatchDifferentFamily - )} - - ); - - expect(screen.getByTestId('codeDanger')).toHaveTextContent(indexFieldType); - }); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx deleted file mode 100644 index ba2e3e2035f68..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiTableFieldDataColumnType } from '@elastic/eui'; -import React from 'react'; - -import { SameFamily } from '../same_family'; -import { CodeDanger, CodeSuccess } from '../../../../../../../../../styles'; -import * as i18n from '../translations'; -import type { EcsBasedFieldMetadata } from '../../../../../../../../../types'; - -export const EMPTY_PLACEHOLDER = '--'; - -export const getIncompatibleMappingsTableColumns = (): Array< - EuiTableFieldDataColumnType -> => [ - { - field: 'indexFieldName', - name: i18n.FIELD, - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'type', - name: i18n.ECS_MAPPING_TYPE_EXPECTED, - render: (type: string) => {type}, - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'indexFieldType', - name: i18n.INDEX_MAPPING_TYPE_ACTUAL, - render: (indexFieldType: string, x) => - x.isInSameFamily ? ( -
- {indexFieldType} - -
- ) : ( - {indexFieldType} - ), - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: i18n.ECS_DESCRIPTION, - sortable: false, - truncateText: false, - width: '35%', - }, -]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.test.tsx deleted file mode 100644 index 8bf2861402c73..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.test.tsx +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, screen } from '@testing-library/react'; -import { omit } from 'lodash/fp'; -import React from 'react'; - -import { - getCustomTableColumns, - getEcsCompliantTableColumns, - getIncompatibleValuesTableColumns, -} from './helpers'; -import { - eventCategory, - eventCategoryWithUnallowedValues, - someField, -} from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; - -describe('helpers', () => { - describe('getCustomTableColumns', () => { - test('it returns the expected columns', () => { - expect(getCustomTableColumns().map((x) => omit('render', x))).toEqual([ - { - field: 'indexFieldName', - name: 'Field', - sortable: true, - truncateText: false, - width: '50%', - }, - { - field: 'indexFieldType', - name: 'Index mapping type', - sortable: true, - truncateText: false, - width: '50%', - }, - ]); - }); - - describe('indexFieldType render()', () => { - test('it renders the indexFieldType', () => { - const columns = getCustomTableColumns(); - const indexFieldTypeRender = columns[1].render; - - render( - - <> - {indexFieldTypeRender != null && - indexFieldTypeRender(someField.indexFieldType, someField)} - - - ); - - expect(screen.getByTestId('indexFieldType')).toHaveTextContent(someField.indexFieldType); - }); - }); - }); - - describe('getEcsCompliantTableColumns', () => { - test('it returns the expected columns', () => { - expect(getEcsCompliantTableColumns().map((x) => omit('render', x))).toEqual([ - { - field: 'indexFieldName', - name: 'Field', - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'type', - name: 'ECS mapping type', - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'allowed_values', - name: 'ECS values', - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: 'ECS description', - sortable: false, - truncateText: false, - width: '35%', - }, - ]); - }); - - describe('type render()', () => { - describe('when `type` exists', () => { - beforeEach(() => { - const columns = getEcsCompliantTableColumns(); - const typeRender = columns[1].render; - - render( - - <>{typeRender != null && typeRender(eventCategory.type, eventCategory)} - - ); - }); - - test('it renders the expected `type`', () => { - expect(screen.getByTestId('type')).toHaveTextContent('keyword'); - }); - - test('it does NOT render the placeholder', () => { - expect(screen.queryByTestId('typePlaceholder')).not.toBeInTheDocument(); - }); - }); - }); - - describe('allowed values render()', () => { - describe('when `allowedValues` exists', () => { - beforeEach(() => { - const columns = getEcsCompliantTableColumns(); - const allowedValuesRender = columns[2].render; - - render( - - <> - {allowedValuesRender != null && - allowedValuesRender(eventCategory.allowed_values, eventCategory)} - - - ); - }); - - test('it renders the expected `AllowedValue` names', () => { - expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( - eventCategory.allowed_values?.map(({ name }) => name).join('') ?? '' - ); - }); - - test('it does NOT render the placeholder', () => { - expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); - }); - }); - - describe('when `allowedValues` is undefined', () => { - const withUndefinedAllowedValues = { - ...eventCategory, - allowed_values: undefined, // <-- - }; - - beforeEach(() => { - const columns = getEcsCompliantTableColumns(); - const allowedValuesRender = columns[2].render; - - render( - - <> - {allowedValuesRender != null && - allowedValuesRender( - withUndefinedAllowedValues.allowed_values, - withUndefinedAllowedValues - )} - - - ); - }); - - test('it does NOT render the `AllowedValue` names', () => { - expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); - }); - - test('it renders the placeholder', () => { - expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); - }); - }); - }); - - describe('description render()', () => { - describe('when `description` exists', () => { - beforeEach(() => { - const columns = getEcsCompliantTableColumns(); - const descriptionRender = columns[3].render; - - render( - - <> - {descriptionRender != null && - descriptionRender(eventCategory.description, eventCategory)} - - - ); - }); - - test('it renders the expected `description`', () => { - expect(screen.getByTestId('description')).toHaveTextContent( - eventCategory.description?.replaceAll('\n', ' ') ?? '' - ); - }); - - test('it does NOT render the placeholder', () => { - expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); - }); - }); - }); - }); - - describe('getIncompatibleValuesTableColumns', () => { - test('it returns the expected columns', () => { - expect(getIncompatibleValuesTableColumns().map((x) => omit('render', x))).toEqual([ - { - field: 'indexFieldName', - name: 'Field', - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'allowed_values', - name: 'ECS values (expected)', - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'indexInvalidValues', - name: 'Document values (actual)', - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: 'ECS description', - sortable: false, - truncateText: false, - width: '35%', - }, - ]); - }); - - describe('allowed values render()', () => { - describe('when `allowedValues` exists', () => { - beforeEach(() => { - const columns = getIncompatibleValuesTableColumns(); - const allowedValuesRender = columns[1].render; - - render( - - <> - {allowedValuesRender != null && - allowedValuesRender(eventCategory.allowed_values, eventCategory)} - - - ); - }); - - test('it renders the expected `AllowedValue` names', () => { - expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( - eventCategory.allowed_values?.map(({ name }) => name).join('') ?? '' - ); - }); - - test('it does NOT render the placeholder', () => { - expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); - }); - }); - - describe('when `allowedValues` is undefined', () => { - const withUndefinedAllowedValues = { - ...eventCategory, - allowed_values: undefined, // <-- - }; - - beforeEach(() => { - const columns = getIncompatibleValuesTableColumns(); - const allowedValuesRender = columns[1].render; - - render( - - <> - {allowedValuesRender != null && - allowedValuesRender( - withUndefinedAllowedValues.allowed_values, - withUndefinedAllowedValues - )} - - - ); - }); - - test('it does NOT render the `AllowedValue` names', () => { - expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); - }); - - test('it renders the placeholder', () => { - expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); - }); - }); - }); - - describe('indexInvalidValues render()', () => { - describe('when `indexInvalidValues` is populated', () => { - beforeEach(() => { - const columns = getIncompatibleValuesTableColumns(); - const indexInvalidValuesRender = columns[2].render; - - render( - - <> - {indexInvalidValuesRender != null && - indexInvalidValuesRender( - eventCategoryWithUnallowedValues.indexInvalidValues, - eventCategoryWithUnallowedValues - )} - - - ); - }); - - test('it renders the expected `indexInvalidValues`', () => { - expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( - 'an_invalid_category (2)theory (1)' - ); - }); - - test('it does NOT render the placeholder', () => { - expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); - }); - }); - - describe('when `indexInvalidValues` is empty', () => { - beforeEach(() => { - const columns = getIncompatibleValuesTableColumns(); - const indexInvalidValuesRender = columns[2].render; - - render( - - <> - {indexInvalidValuesRender != null && - indexInvalidValuesRender(eventCategory.indexInvalidValues, eventCategory)} - - - ); - }); - - test('it does NOT render the index invalid values', () => { - expect(screen.queryByTestId('indexInvalidValues')).not.toBeInTheDocument(); - }); - - test('it renders the placeholder', () => { - expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument(); - }); - }); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.tsx deleted file mode 100644 index 26e3a038b1ffe..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/helpers.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiTableFieldDataColumnType } from '@elastic/eui'; -import { EuiCode } from '@elastic/eui'; -import React from 'react'; - -import { EcsAllowedValues } from './ecs_allowed_values'; -import { IndexInvalidValues } from './index_invalid_values'; -import { CodeSuccess } from '../../../../../../../../styles'; -import * as i18n from './translations'; -import type { - AllowedValue, - CustomFieldMetadata, - EcsBasedFieldMetadata, - UnallowedValueCount, -} from '../../../../../../../../types'; - -export const EMPTY_PLACEHOLDER = '--'; - -export const getCustomTableColumns = (): Array< - EuiTableFieldDataColumnType -> => [ - { - field: 'indexFieldName', - name: i18n.FIELD, - sortable: true, - truncateText: false, - width: '50%', - }, - { - field: 'indexFieldType', - name: i18n.INDEX_MAPPING_TYPE, - render: (indexFieldType: string) => ( - {indexFieldType} - ), - sortable: true, - truncateText: false, - width: '50%', - }, -]; - -export const getEcsCompliantTableColumns = (): Array< - EuiTableFieldDataColumnType -> => [ - { - field: 'indexFieldName', - name: i18n.FIELD, - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'type', - name: i18n.ECS_MAPPING_TYPE, - render: (type: string) => {type}, - sortable: true, - truncateText: false, - width: '25%', - }, - { - field: 'allowed_values', - name: i18n.ECS_VALUES, - render: (allowedValues: AllowedValue[] | undefined) => ( - - ), - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: i18n.ECS_DESCRIPTION, - render: (description: string) => {description}, - sortable: false, - truncateText: false, - width: '35%', - }, -]; - -export const getIncompatibleValuesTableColumns = (): Array< - EuiTableFieldDataColumnType -> => [ - { - field: 'indexFieldName', - name: i18n.FIELD, - sortable: true, - truncateText: false, - width: '15%', - }, - { - field: 'allowed_values', - name: i18n.ECS_VALUES_EXPECTED, - render: (allowedValues: AllowedValue[] | undefined) => ( - - ), - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'indexInvalidValues', - name: i18n.DOCUMENT_VALUES_ACTUAL, - render: (indexInvalidValues: UnallowedValueCount[]) => ( - - ), - sortable: false, - truncateText: false, - width: '25%', - }, - { - field: 'description', - name: i18n.ECS_DESCRIPTION, - sortable: false, - truncateText: false, - width: '35%', - }, -]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/translations.ts deleted file mode 100644 index 95b968f0cd34e..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/translations.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ECS_VALUES = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesColumn', - { - defaultMessage: 'ECS values', - } -); - -export const ECS_VALUES_EXPECTED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesExpectedColumn', - { - defaultMessage: 'ECS values (expected)', - } -); - -export const ECS_DESCRIPTION = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsDescriptionColumn', - { - defaultMessage: 'ECS description', - } -); - -export const ECS_MAPPING_TYPE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeColumn', - { - defaultMessage: 'ECS mapping type', - } -); - -export const ECS_MAPPING_TYPE_EXPECTED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeExpectedColumn', - { - defaultMessage: 'ECS mapping type (expected)', - } -); - -export const DOCUMENT_VALUES_ACTUAL = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.documentValuesActualColumn', - { - defaultMessage: 'Document values (actual)', - } -); - -export const INDEX_MAPPING_TYPE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeColumn', - { - defaultMessage: 'Index mapping type', - } -); - -export const INDEX_MAPPING_TYPE_ACTUAL = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeActualColumn', - { - defaultMessage: 'Index mapping type (actual)', - } -); - -export const FIELD = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.fieldColumn', - { - defaultMessage: 'Field', - } -); - -export const SEARCH_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder', - { - defaultMessage: 'Search fields', - } -); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.test.ts deleted file mode 100644 index 5061a818e17fd..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import numeral from '@elastic/numeral'; -import { EcsVersion } from '@elastic/ecs'; - -import { ECS_IS_A_PERMISSIVE_SCHEMA } from '../../../translations'; -import { - getAllCustomMarkdownComments, - getCustomMarkdownComment, - showCustomCallout, -} from './helpers'; -import { - hostNameKeyword, - someField, -} from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { mockPartitionedFieldMetadata } from '../../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { EMPTY_STAT } from '../../../../../../../../constants'; - -const defaultBytesFormat = '0,0.[0]b'; -const formatBytes = (value: number | undefined) => - value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; - -const defaultNumberFormat = '0,0.[000]'; -const formatNumber = (value: number | undefined) => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; - -describe('helpers', () => { - describe('getCustomMarkdownComment', () => { - test('it returns a comment for custom fields with the expected field counts and ECS version', () => { - expect(getCustomMarkdownComment({ customFieldMetadata: [hostNameKeyword, someField] })) - .toEqual(`#### 2 Custom field mappings - -These fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}. - -${ECS_IS_A_PERMISSIVE_SCHEMA} -`); - }); - }); - - describe('showCustomCallout', () => { - test('it returns false when `enrichedFieldMetadata` is empty', () => { - expect(showCustomCallout([])).toBe(false); - }); - - test('it returns true when `enrichedFieldMetadata` is NOT empty', () => { - expect(showCustomCallout([someField])).toBe(true); - }); - }); - - describe('getAllCustomMarkdownComments', () => { - test('it returns the expected comment', () => { - expect( - getAllCustomMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, - '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', - ]); - }); - - test('it returns the expected comment without ILM Phase when isILMAvailable is false', () => { - expect( - getAllCustomMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, - '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', - ]); - }); - - test('it returns the expected comment without Size when Size is undefined', () => { - expect( - getAllCustomMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: undefined, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, - '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', - ]); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.ts deleted file mode 100644 index 8beb64cfce88e..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/helpers.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; - -import { FIELD, INDEX_MAPPING_TYPE } from '../compare_fields_table/translations'; -import { - getSummaryMarkdownComment, - getCustomMarkdownTableRows, - getMarkdownComment, - getMarkdownTable, - getTabCountsMarkdownComment, - getSummaryTableMarkdownComment, -} from '../../utils/markdown'; -import * as i18n from '../../../translations'; -import type { - CustomFieldMetadata, - IlmPhase, - PartitionedFieldMetadata, -} from '../../../../../../../../types'; - -export const getCustomMarkdownComment = ({ - customFieldMetadata, -}: { - customFieldMetadata: CustomFieldMetadata[]; -}): string => - getMarkdownComment({ - suggestedAction: `${i18n.CUSTOM_CALLOUT({ - fieldCount: customFieldMetadata.length, - version: EcsVersion, - })} - -${i18n.ECS_IS_A_PERMISSIVE_SCHEMA} -`, - title: i18n.CUSTOM_CALLOUT_TITLE(customFieldMetadata.length), - }); - -export const showCustomCallout = (customFieldMetadata: CustomFieldMetadata[]): boolean => - customFieldMetadata.length > 0; - -export const getAllCustomMarkdownComments = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}: { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - isILMAvailable: boolean; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -}): string[] => [ - getSummaryMarkdownComment(indexName), - getSummaryTableMarkdownComment({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }), - getTabCountsMarkdownComment(partitionedFieldMetadata), - getCustomMarkdownComment({ - customFieldMetadata: partitionedFieldMetadata.custom, - }), - getMarkdownTable({ - enrichedFieldMetadata: partitionedFieldMetadata.custom, - getMarkdownTableRows: getCustomMarkdownTableRows, - headerNames: [FIELD, INDEX_MAPPING_TYPE], - title: i18n.CUSTOM_FIELDS_TABLE_TITLE(indexName), - }), -]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/ecs_compliant_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/ecs_compliant_tab/index.tsx deleted file mode 100644 index 7455ae3f482b9..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/ecs_compliant_tab/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; - -import { EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; - -import { CompareFieldsTable } from '../compare_fields_table'; -import { getEcsCompliantTableColumns } from '../compare_fields_table/helpers'; -import { EmptyPromptBody } from '../../../empty_prompt_body'; -import { EmptyPromptTitle } from '../../../empty_prompt_title'; -import { showMissingTimestampCallout } from '../helpers'; -import { CalloutItem } from '../styles'; -import * as i18n from '../../../translations'; -import type { PartitionedFieldMetadata } from '../../../../../../../../types'; - -const EmptyPromptContainer = styled.div` - width: 100%; -`; - -interface Props { - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; -} - -const EcsCompliantTabComponent: React.FC = ({ indexName, partitionedFieldMetadata }) => { - const emptyPromptBody = useMemo(() => , []); - const title = useMemo(() => , []); - - return ( -
- {!showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) ? ( - <> - -

- {i18n.ECS_COMPLIANT_CALLOUT({ - fieldCount: partitionedFieldMetadata.ecsCompliant.length, - version: EcsVersion, - })} -

- {i18n.PRE_BUILT_DETECTION_ENGINE_RULES_WORK} - {i18n.CUSTOM_DETECTION_ENGINE_RULES_WORK} - {i18n.PAGES_DISPLAY_EVENTS} - {i18n.OTHER_APP_CAPABILITIES_WORK_PROPERLY} - {i18n.ECS_COMPLIANT_MAPPINGS_ARE_FULLY_SUPPORTED} -
- - - - ) : ( - - - - )} -
- ); -}; - -EcsCompliantTabComponent.displayName = 'EcsCompliantTabComponent'; - -export const EcsCompliantTab = React.memo(EcsCompliantTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.test.tsx deleted file mode 100644 index 4c30702fcef36..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.test.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash/fp'; - -import { - eventCategory, - timestamp, -} from '../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { mockPartitionedFieldMetadata } from '../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { mockStatsAuditbeatIndex } from '../../../../../../../mock/stats/mock_stats_packetbeat_index'; -import { - getEcsCompliantBadgeColor, - getMissingTimestampComment, - getTabs, - showMissingTimestampCallout, -} from './helpers'; - -describe('helpers', () => { - describe('getMissingTimestampComment', () => { - test('it returns the expected comment', () => { - expect(getMissingTimestampComment()).toEqual( - '#### Missing an @timestamp (date) field mapping for this index\n\nConsider adding an @timestamp (date) field mapping to this index, as required by the Elastic Common Schema (ECS), because:\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n' - ); - }); - }); - - describe('showMissingTimestampCallout', () => { - test('it returns true when `enrichedFieldMetadata` is empty', () => { - expect(showMissingTimestampCallout([])).toBe(true); - }); - - test('it returns false when `enrichedFieldMetadata` contains an @timestamp field', () => { - expect(showMissingTimestampCallout([timestamp, eventCategory])).toBe(false); - }); - - test('it returns true when `enrichedFieldMetadata` does NOT contain an @timestamp field', () => { - expect(showMissingTimestampCallout([eventCategory])).toBe(true); - }); - }); - - describe('getEcsCompliantBadgeColor', () => { - test('it returns the expected color for the ECS compliant data when the data includes an @timestamp', () => { - expect(getEcsCompliantBadgeColor(mockPartitionedFieldMetadata)).toBe('hollow'); - }); - - test('it returns the expected color for the ECS compliant data does NOT includes an @timestamp', () => { - const noTimestamp = { - ...mockPartitionedFieldMetadata, - ecsCompliant: mockPartitionedFieldMetadata.ecsCompliant.filter( - ({ name }) => name !== '@timestamp' - ), - }; - - expect(getEcsCompliantBadgeColor(noTimestamp)).toEqual('danger'); - }); - }); - - describe('getTabs', () => { - test('it returns the expected tabs', () => { - expect( - getTabs({ - docsCount: 4, - formatBytes: jest.fn(), - formatNumber: jest.fn(), - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - stats: mockStatsAuditbeatIndex, - }).map((x) => omit(['append', 'content'], x)) - ).toEqual([ - { - id: 'incompatibleTab', - name: 'Incompatible fields', - }, - { - id: 'sameFamilyTab', - name: 'Same family', - }, - { - id: 'customTab', - name: 'Custom fields', - }, - { - id: 'ecsCompliantTab', - name: 'ECS compliant fields', - }, - { - id: 'allTab', - name: 'All fields', - }, - ]); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx deleted file mode 100644 index 41b0c49e66ed1..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiBadge } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { INCOMPATIBLE_FIELDS } from '../../../../../../../translations'; -import { getSizeInBytes } from '../../../../../../../utils/stats'; -import { getIncompatibleStatBadgeColor } from '../../../../../../../utils/get_incompatible_stat_badge_color'; -import { AllTab } from './all_tab'; -import { CustomTab } from './custom_tab'; -import { EcsCompliantTab } from './ecs_compliant_tab'; -import { IncompatibleTab } from './incompatible_tab'; -import { - ALL_TAB_ID, - CUSTOM_TAB_ID, - ECS_COMPLIANT_TAB_ID, - INCOMPATIBLE_TAB_ID, - SAME_FAMILY_TAB_ID, -} from '../constants'; -import * as i18n from '../../translations'; -import { SameFamilyTab } from './same_family_tab'; -import type { - EcsBasedFieldMetadata, - IlmPhase, - MeteringStatsIndex, - PartitionedFieldMetadata, -} from '../../../../../../../types'; -import { getMarkdownComment } from '../utils/markdown'; - -export const getMissingTimestampComment = (): string => - getMarkdownComment({ - suggestedAction: `${i18n.MISSING_TIMESTAMP_CALLOUT} - -${i18n.DETECTION_ENGINE_RULES_MAY_NOT_MATCH} -${i18n.PAGES_MAY_NOT_DISPLAY_EVENTS} -`, - title: i18n.MISSING_TIMESTAMP_CALLOUT_TITLE, - }); - -export const showMissingTimestampCallout = ( - ecsBasedFieldMetadata: EcsBasedFieldMetadata[] -): boolean => !ecsBasedFieldMetadata.some((x) => x.name === '@timestamp'); - -export const getEcsCompliantBadgeColor = ( - partitionedFieldMetadata: PartitionedFieldMetadata -): string => - showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) ? 'danger' : 'hollow'; - -const StyledBadge = styled(EuiBadge)` - text-align: right; - cursor: pointer; -`; - -export const getTabs = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - partitionedFieldMetadata, - patternDocsCount, - stats, -}: { - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - docsCount: number; - ilmPhase: IlmPhase | undefined; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - stats: Record | null; -}) => [ - { - append: ( - - {partitionedFieldMetadata.incompatible.length} - - ), - content: ( - - ), - id: INCOMPATIBLE_TAB_ID, - name: INCOMPATIBLE_FIELDS, - }, - { - append: {partitionedFieldMetadata.sameFamily.length}, - content: ( - - ), - id: SAME_FAMILY_TAB_ID, - name: i18n.SAME_FAMILY, - }, - { - append: {partitionedFieldMetadata.custom.length}, - content: ( - - ), - id: CUSTOM_TAB_ID, - name: i18n.CUSTOM_FIELDS, - }, - { - append: ( - - {partitionedFieldMetadata.ecsCompliant.length} - - ), - content: ( - - ), - id: ECS_COMPLIANT_TAB_ID, - name: i18n.ECS_COMPLIANT_FIELDS, - }, - { - append: {partitionedFieldMetadata.all.length}, - content: , - id: ALL_TAB_ID, - name: i18n.ALL_FIELDS, - }, -]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.test.ts deleted file mode 100644 index 5d0e66daff88a..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.test.ts +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import numeral from '@elastic/numeral'; -import { EcsVersion } from '@elastic/ecs'; - -import { - getAllIncompatibleMarkdownComments, - getIncompatibleFieldsMarkdownComment, - getIncompatibleFieldsMarkdownTablesComment, - getIncompatibleMappings, - getIncompatibleMappingsFields, - getIncompatibleValues, - getIncompatibleValuesFields, - showInvalidCallout, -} from './helpers'; -import { EMPTY_STAT } from '../../../../../../../../constants'; -import { - DETECTION_ENGINE_RULES_MAY_NOT_MATCH, - MAPPINGS_THAT_CONFLICT_WITH_ECS, - PAGES_MAY_NOT_DISPLAY_EVENTS, -} from '../../../translations'; -import { mockPartitionedFieldMetadata } from '../../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { PartitionedFieldMetadata } from '../../../../../../../../types'; - -describe('helpers', () => { - describe('getIncompatibleFieldsMarkdownComment', () => { - test('it returns the expected counts and ECS version', () => { - expect(getIncompatibleFieldsMarkdownComment(11)).toEqual(`#### 11 incompatible fields - -Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}. - -${DETECTION_ENGINE_RULES_MAY_NOT_MATCH} -${PAGES_MAY_NOT_DISPLAY_EVENTS} -${MAPPINGS_THAT_CONFLICT_WITH_ECS} -`); - }); - }); - - describe('showInvalidCallout', () => { - test('it returns false when the `enrichedFieldMetadata` is empty', () => { - expect(showInvalidCallout([])).toBe(false); - }); - - test('it returns true when the `enrichedFieldMetadata` is NOT empty', () => { - expect(showInvalidCallout(mockPartitionedFieldMetadata.incompatible)).toBe(true); - }); - }); - - describe('getIncompatibleMappings', () => { - test('it (only) returns the mappings where type !== indexFieldType', () => { - expect(getIncompatibleMappings(mockPartitionedFieldMetadata.incompatible)).toEqual([ - { - dashed_name: 'host-name', - description: - 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', - flat_name: 'host.name', - hasEcsMetadata: true, - ignore_above: 1024, - indexFieldName: 'host.name', - indexFieldType: 'text', - indexInvalidValues: [], - isEcsCompliant: false, - isInSameFamily: false, - level: 'core', - name: 'name', - normalize: [], - short: 'Name of the host.', - type: 'keyword', - }, - { - dashed_name: 'source-ip', - description: 'IP address of the source (IPv4 or IPv6).', - flat_name: 'source.ip', - hasEcsMetadata: true, - indexFieldName: 'source.ip', - indexFieldType: 'text', - indexInvalidValues: [], - isEcsCompliant: false, - isInSameFamily: false, - level: 'core', - name: 'ip', - normalize: [], - short: 'IP address of the source.', - type: 'ip', - }, - ]); - }); - - test('it filters-out ECS complaint fields', () => { - expect(getIncompatibleMappings(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); - }); - }); - - describe('getIncompatibleMappingsFields', () => { - test('it (only) returns the fields where type !== indexFieldType', () => { - expect(getIncompatibleMappingsFields(mockPartitionedFieldMetadata.incompatible)).toEqual([ - 'host.name', - 'source.ip', - ]); - }); - - test('it filters-out ECS complaint fields', () => { - expect(getIncompatibleMappingsFields(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); - }); - }); - - describe('getIncompatibleValues', () => { - test('it (only) returns the mappings with indexInvalidValues', () => { - expect(getIncompatibleValues(mockPartitionedFieldMetadata.incompatible)).toEqual([ - { - allowed_values: [ - { - description: - 'Events in this category are related to the challenge and response process in which credentials are supplied and verified to allow the creation of a session. Common sources for these logs are Windows event logs and ssh logs. Visualize and analyze events in this category to look for failed logins, and other authentication-related activity.', - expected_event_types: ['start', 'end', 'info'], - name: 'authentication', - }, - { - description: - 'Events in the configuration category have to deal with creating, modifying, or deleting the settings or parameters of an application, process, or system.\nExample sources include security policy change logs, configuration auditing logging, and system integrity monitoring.', - expected_event_types: ['access', 'change', 'creation', 'deletion', 'info'], - name: 'configuration', - }, - { - description: - 'The database category denotes events and metrics relating to a data storage and retrieval system. Note that use of this category is not limited to relational database systems. Examples include event logs from MS SQL, MySQL, Elasticsearch, MongoDB, etc. Use this category to visualize and analyze database activity such as accesses and changes.', - expected_event_types: ['access', 'change', 'info', 'error'], - name: 'database', - }, - { - description: - 'Events in the driver category have to do with operating system device drivers and similar software entities such as Windows drivers, kernel extensions, kernel modules, etc.\nUse events and metrics in this category to visualize and analyze driver-related activity and status on hosts.', - expected_event_types: ['change', 'end', 'info', 'start'], - name: 'driver', - }, - { - description: - 'This category is used for events relating to email messages, email attachments, and email network or protocol activity.\nEmails events can be produced by email security gateways, mail transfer agents, email cloud service providers, or mail server monitoring applications.', - expected_event_types: ['info'], - name: 'email', - }, - { - description: - 'Relating to a set of information that has been created on, or has existed on a filesystem. Use this category of events to visualize and analyze the creation, access, and deletions of files. Events in this category can come from both host-based and network-based sources. An example source of a network-based detection of a file transfer would be the Zeek file.log.', - expected_event_types: ['change', 'creation', 'deletion', 'info'], - name: 'file', - }, - { - description: - 'Use this category to visualize and analyze information such as host inventory or host lifecycle events.\nMost of the events in this category can usually be observed from the outside, such as from a hypervisor or a control plane\'s point of view. Some can also be seen from within, such as "start" or "end".\nNote that this category is for information about hosts themselves; it is not meant to capture activity "happening on a host".', - expected_event_types: ['access', 'change', 'end', 'info', 'start'], - name: 'host', - }, - { - description: - 'Identity and access management (IAM) events relating to users, groups, and administration. Use this category to visualize and analyze IAM-related logs and data from active directory, LDAP, Okta, Duo, and other IAM systems.', - expected_event_types: [ - 'admin', - 'change', - 'creation', - 'deletion', - 'group', - 'info', - 'user', - ], - name: 'iam', - }, - { - description: - 'Relating to intrusion detections from IDS/IPS systems and functions, both network and host-based. Use this category to visualize and analyze intrusion detection alerts from systems such as Snort, Suricata, and Palo Alto threat detections.', - expected_event_types: ['allowed', 'denied', 'info'], - name: 'intrusion_detection', - }, - { - description: - 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.', - expected_event_types: ['info'], - name: 'malware', - }, - { - description: - 'Relating to all network activity, including network connection lifecycle, network traffic, and essentially any event that includes an IP address. Many events containing decoded network protocol transactions fit into this category. Use events in this category to visualize or analyze counts of network ports, protocols, addresses, geolocation information, etc.', - expected_event_types: [ - 'access', - 'allowed', - 'connection', - 'denied', - 'end', - 'info', - 'protocol', - 'start', - ], - name: 'network', - }, - { - description: - 'Relating to software packages installed on hosts. Use this category to visualize and analyze inventory of software installed on various hosts, or to determine host vulnerability in the absence of vulnerability scan data.', - expected_event_types: [ - 'access', - 'change', - 'deletion', - 'info', - 'installation', - 'start', - ], - name: 'package', - }, - { - description: - 'Use this category of events to visualize and analyze process-specific information such as lifecycle events or process ancestry.', - expected_event_types: ['access', 'change', 'end', 'info', 'start'], - name: 'process', - }, - { - description: - 'Having to do with settings and assets stored in the Windows registry. Use this category to visualize and analyze activity such as registry access and modifications.', - expected_event_types: ['access', 'change', 'creation', 'deletion'], - name: 'registry', - }, - { - description: - 'The session category is applied to events and metrics regarding logical persistent connections to hosts and services. Use this category to visualize and analyze interactive or automated persistent connections between assets. Data for this category may come from Windows Event logs, SSH logs, or stateless sessions such as HTTP cookie-based sessions, etc.', - expected_event_types: ['start', 'end', 'info'], - name: 'session', - }, - { - description: - "Use this category to visualize and analyze events describing threat actors' targets, motives, or behaviors.", - expected_event_types: ['indicator'], - name: 'threat', - }, - { - description: - 'Relating to vulnerability scan results. Use this category to analyze vulnerabilities detected by Tenable, Qualys, internal scanners, and other vulnerability management sources.', - expected_event_types: ['info'], - name: 'vulnerability', - }, - { - description: - 'Relating to web server access. Use this category to create a dashboard of web server/proxy activity from apache, IIS, nginx web servers, etc. Note: events from network observers such as Zeek http log may also be included in this category.', - expected_event_types: ['access', 'error', 'info'], - name: 'web', - }, - ], - dashed_name: 'event-category', - description: - 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', - example: 'authentication', - flat_name: 'event.category', - ignore_above: 1024, - level: 'core', - name: 'category', - normalize: ['array'], - short: 'Event category. The second categorization field in the hierarchy.', - type: 'keyword', - indexFieldName: 'event.category', - indexFieldType: 'keyword', - indexInvalidValues: [ - { count: 2, fieldName: 'an_invalid_category' }, - { count: 1, fieldName: 'theory' }, - ], - hasEcsMetadata: true, - isEcsCompliant: false, - isInSameFamily: false, - }, - ]); - }); - - test('it filters-out ECS complaint fields', () => { - expect(getIncompatibleValues(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); - }); - }); - - describe('getIncompatibleValuesFields', () => { - test('it (only) returns the fields with indexInvalidValues', () => { - expect(getIncompatibleValuesFields(mockPartitionedFieldMetadata.incompatible)).toEqual([ - 'event.category', - ]); - }); - - test('it filters-out ECS complaint fields', () => { - expect(getIncompatibleValuesFields(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); - }); - }); - - describe('getIncompatibleFieldsMarkdownTablesComment', () => { - test('it returns the expected comment when the index has `incompatibleMappings` and `incompatibleValues`', () => { - expect( - getIncompatibleFieldsMarkdownTablesComment({ - incompatibleMappings: [ - mockPartitionedFieldMetadata.incompatible[1], - mockPartitionedFieldMetadata.incompatible[2], - ], - incompatibleValues: [mockPartitionedFieldMetadata.incompatible[0]], - indexName: 'auditbeat-custom-index-1', - }) - ).toEqual( - '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n' - ); - }); - - test('it returns the expected comment when the index does NOT have `incompatibleMappings` and `incompatibleValues`', () => { - expect( - getIncompatibleFieldsMarkdownTablesComment({ - incompatibleMappings: [], // <-- no `incompatibleMappings` - incompatibleValues: [], // <-- no `incompatibleValues` - indexName: 'auditbeat-custom-index-1', - }) - ).toEqual('\n\n\n'); - }); - }); - - describe('getAllIncompatibleMarkdownComments', () => { - const defaultBytesFormat = '0,0.[0]b'; - const formatBytes = (value: number | undefined) => - value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; - - const defaultNumberFormat = '0,0.[000]'; - const formatNumber = (value: number | undefined) => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; - - test('it returns the expected collection of comments', () => { - expect( - getAllIncompatibleMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - isILMAvailable: true, - indexName: 'auditbeat-custom-index-1', - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n${DETECTION_ENGINE_RULES_MAY_NOT_MATCH}\n${PAGES_MAY_NOT_DISPLAY_EVENTS}\n${MAPPINGS_THAT_CONFLICT_WITH_ECS}\n`, - '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', - ]); - }); - - test('it returns the expected comment when `incompatible` is empty', () => { - const emptyIncompatible: PartitionedFieldMetadata = { - ...mockPartitionedFieldMetadata, - incompatible: [], // <-- empty - }; - - expect( - getAllIncompatibleMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: true, - partitionedFieldMetadata: emptyIncompatible, - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 | `unmanaged` | 27.7KB |\n\n', - '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - '\n\n\n', - ]); - }); - - test('it returns the expected comment when `isILMAvailable` is false', () => { - const emptyIncompatible: PartitionedFieldMetadata = { - ...mockPartitionedFieldMetadata, - incompatible: [], // <-- empty - }; - - expect( - getAllIncompatibleMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: emptyIncompatible, - patternDocsCount: 57410, - sizeInBytes: undefined, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 |\n\n', - '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - '\n\n\n', - ]); - }); - - test('it returns the expected comment when `sizeInBytes` is not an integer', () => { - const emptyIncompatible: PartitionedFieldMetadata = { - ...mockPartitionedFieldMetadata, - incompatible: [], // <-- empty - }; - - expect( - getAllIncompatibleMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: emptyIncompatible, - patternDocsCount: 57410, - sizeInBytes: undefined, - }) - ).toEqual([ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 |\n\n', - '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - '\n\n\n', - ]); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.ts deleted file mode 100644 index c7c93f67d80bb..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; - -import { escapeNewlines } from '../../../../../../../../utils/markdown'; -import { - getSummaryMarkdownComment, - getIncompatibleMappingsMarkdownTableRows, - getIncompatibleValuesMarkdownTableRows, - getMarkdownComment, - getMarkdownTable, - getTabCountsMarkdownComment, - getSummaryTableMarkdownComment, -} from '../../utils/markdown'; -import * as i18n from '../../../translations'; -import type { - EcsBasedFieldMetadata, - IlmPhase, - PartitionedFieldMetadata, -} from '../../../../../../../../types'; -import { - INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, - INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE, -} from './translations'; -import { - FIELD, - ECS_MAPPING_TYPE_EXPECTED, - INDEX_MAPPING_TYPE_ACTUAL, - DOCUMENT_VALUES_ACTUAL, - ECS_VALUES_EXPECTED, -} from '../compare_fields_table/translations'; -import { getIsInSameFamily } from '../../../utils/get_is_in_same_family'; - -export const getIncompatibleFieldsMarkdownComment = (incompatible: number): string => - getMarkdownComment({ - suggestedAction: `${i18n.INCOMPATIBLE_CALLOUT(EcsVersion)} - -${i18n.DETECTION_ENGINE_RULES_MAY_NOT_MATCH} -${i18n.PAGES_MAY_NOT_DISPLAY_EVENTS} -${i18n.MAPPINGS_THAT_CONFLICT_WITH_ECS} -`, - title: i18n.INCOMPATIBLE_CALLOUT_TITLE(incompatible), - }); - -export const showInvalidCallout = (ecsBasedFieldMetadata: EcsBasedFieldMetadata[]): boolean => - ecsBasedFieldMetadata.length > 0; - -export const getIncompatibleMappings = ( - ecsBasedFieldMetadata: EcsBasedFieldMetadata[] -): EcsBasedFieldMetadata[] => - ecsBasedFieldMetadata.filter( - (x) => - !x.isEcsCompliant && - x.type !== x.indexFieldType && - !getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType }) - ); - -export const getIncompatibleMappingsFields = ( - ecsBasedFieldMetadata: EcsBasedFieldMetadata[] -): string[] => - ecsBasedFieldMetadata.reduce((acc, x) => { - if ( - !x.isEcsCompliant && - x.type !== x.indexFieldType && - !getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType }) - ) { - const field = escapeNewlines(x.indexFieldName); - if (field != null) { - return [...acc, field]; - } - } - return acc; - }, []); - -export const getSameFamilyFields = (ecsBasedFieldMetadata: EcsBasedFieldMetadata[]): string[] => - ecsBasedFieldMetadata.reduce((acc, x) => { - if (!x.isEcsCompliant && x.type !== x.indexFieldType && x.isInSameFamily) { - const field = escapeNewlines(x.indexFieldName); - if (field != null) { - return [...acc, field]; - } - } - return acc; - }, []); - -export const getIncompatibleValues = ( - ecsBasedFieldMetadata: EcsBasedFieldMetadata[] -): EcsBasedFieldMetadata[] => - ecsBasedFieldMetadata.filter((x) => !x.isEcsCompliant && x.indexInvalidValues.length > 0); - -export const getIncompatibleValuesFields = ( - ecsBasedFieldMetadata: EcsBasedFieldMetadata[] -): string[] => - ecsBasedFieldMetadata.reduce((acc, x) => { - if (!x.isEcsCompliant && x.indexInvalidValues.length > 0) { - const field = escapeNewlines(x.indexFieldName); - if (field != null) { - return [...acc, field]; - } - } - return acc; - }, []); - -export const getIncompatibleFieldsMarkdownTablesComment = ({ - incompatibleMappings, - incompatibleValues, - indexName, -}: { - incompatibleMappings: EcsBasedFieldMetadata[]; - incompatibleValues: EcsBasedFieldMetadata[]; - indexName: string; -}): string => ` -${ - incompatibleMappings.length > 0 - ? getMarkdownTable({ - enrichedFieldMetadata: incompatibleMappings, - getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, - headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], - title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), - }) - : '' -} -${ - incompatibleValues.length > 0 - ? getMarkdownTable({ - enrichedFieldMetadata: incompatibleValues, - getMarkdownTableRows: getIncompatibleValuesMarkdownTableRows, - headerNames: [FIELD, ECS_VALUES_EXPECTED, DOCUMENT_VALUES_ACTUAL], - title: INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE(indexName), - }) - : '' -} -`; - -export const getAllIncompatibleMarkdownComments = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}: { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - isILMAvailable: boolean; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -}): string[] => { - const incompatibleMappings = getIncompatibleMappings(partitionedFieldMetadata.incompatible); - const incompatibleValues = getIncompatibleValues(partitionedFieldMetadata.incompatible); - - const incompatibleFieldsMarkdownComment = - partitionedFieldMetadata.incompatible.length > 0 - ? getIncompatibleFieldsMarkdownComment(partitionedFieldMetadata.incompatible.length) - : ''; - - return [ - getSummaryMarkdownComment(indexName), - getSummaryTableMarkdownComment({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }), - getTabCountsMarkdownComment(partitionedFieldMetadata), - incompatibleFieldsMarkdownComment, - getIncompatibleFieldsMarkdownTablesComment({ - incompatibleMappings, - incompatibleValues, - indexName, - }), - ].filter((x) => x !== ''); -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/index.tsx deleted file mode 100644 index d24e82192291f..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; - -import { IncompatibleCallout } from '../callouts/incompatible_callout'; -import { CompareFieldsTable } from '../compare_fields_table'; -import { getIncompatibleMappingsTableColumns } from '../compare_fields_table/get_incompatible_mappings_table_columns'; -import { getIncompatibleValuesTableColumns } from '../compare_fields_table/helpers'; -import { EmptyPromptBody } from '../../../empty_prompt_body'; -import { EmptyPromptTitle } from '../../../empty_prompt_title'; -import { - getAllIncompatibleMarkdownComments, - getIncompatibleMappings, - getIncompatibleValues, - showInvalidCallout, -} from './helpers'; -import * as i18n from '../../../translations'; -import { - INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, - INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE, -} from './translations'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../../../../../../types'; -import { useDataQualityContext } from '../../../../../../../../data_quality_context'; -import { StickyActions } from '../sticky_actions'; - -interface Props { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -} - -const IncompatibleTabComponent: React.FC = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}) => { - const body = useMemo(() => , []); - const title = useMemo(() => , []); - const incompatibleMappings = useMemo( - () => getIncompatibleMappings(partitionedFieldMetadata.incompatible), - [partitionedFieldMetadata.incompatible] - ); - const incompatibleValues = useMemo( - () => getIncompatibleValues(partitionedFieldMetadata.incompatible), - [partitionedFieldMetadata.incompatible] - ); - - const { isILMAvailable } = useDataQualityContext(); - - const markdownComment: string = useMemo( - () => - getAllIncompatibleMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }).join('\n'), - [ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - ] - ); - - return ( -
- {showInvalidCallout(partitionedFieldMetadata.incompatible) ? ( - <> - - - <> - {incompatibleMappings.length > 0 && ( - <> - - - - - )} - - - <> - {incompatibleValues.length > 0 && ( - <> - - - - - )} - - - - - - ) : ( - - )} -
- ); -}; - -IncompatibleTabComponent.displayName = 'IncompatibleTabComponent'; - -export const IncompatibleTab = React.memo(IncompatibleTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/translations.ts deleted file mode 100644 index 3be9a55333ad2..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/translations.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle', - { - values: { indexName }, - defaultMessage: 'Incompatible field mappings - {indexName}', - } - ); - -export const INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle', - { - values: { indexName }, - defaultMessage: 'Incompatible field values - {indexName}', - } - ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.ts deleted file mode 100644 index e96e62704e812..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EcsVersion } from '@elastic/ecs'; - -import { - FIELD, - ECS_MAPPING_TYPE_EXPECTED, - INDEX_MAPPING_TYPE_ACTUAL, -} from '../compare_fields_table/translations'; -import { - getSummaryMarkdownComment, - getIncompatibleMappingsMarkdownTableRows, - getMarkdownComment, - getMarkdownTable, - getTabCountsMarkdownComment, - getSummaryTableMarkdownComment, -} from '../../utils/markdown'; -import * as i18n from '../../../translations'; -import { SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE } from './translations'; -import type { - EcsBasedFieldMetadata, - IlmPhase, - PartitionedFieldMetadata, -} from '../../../../../../../../types'; - -export const getSameFamilyMarkdownComment = (fieldsInSameFamily: number): string => - getMarkdownComment({ - suggestedAction: `${i18n.SAME_FAMILY_CALLOUT({ - fieldCount: fieldsInSameFamily, - version: EcsVersion, - })} - -${i18n.FIELDS_WITH_MAPPINGS_SAME_FAMILY} -`, - title: i18n.SAME_FAMILY_CALLOUT_TITLE(fieldsInSameFamily), - }); - -export const getSameFamilyMappings = ( - enrichedFieldMetadata: EcsBasedFieldMetadata[] -): EcsBasedFieldMetadata[] => enrichedFieldMetadata.filter((x) => x.isInSameFamily); - -export const getSameFamilyMarkdownTablesComment = ({ - sameFamilyMappings, - indexName, -}: { - sameFamilyMappings: EcsBasedFieldMetadata[]; - indexName: string; -}): string => ` -${ - sameFamilyMappings.length > 0 - ? getMarkdownTable({ - enrichedFieldMetadata: sameFamilyMappings, - getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, - headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], - title: SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE(indexName), - }) - : '' -} -`; - -export const getAllSameFamilyMarkdownComments = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}: { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - isILMAvailable: boolean; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -}): string[] => { - const sameFamilyMappings = getSameFamilyMappings(partitionedFieldMetadata.sameFamily); - const fieldsInSameFamily = partitionedFieldMetadata.sameFamily.length; - - const incompatibleFieldsMarkdownComment = - partitionedFieldMetadata.sameFamily.length > 0 - ? getSameFamilyMarkdownComment(fieldsInSameFamily) - : ''; - - return [ - getSummaryMarkdownComment(indexName), - getSummaryTableMarkdownComment({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }), - getTabCountsMarkdownComment(partitionedFieldMetadata), - incompatibleFieldsMarkdownComment, - getSameFamilyMarkdownTablesComment({ - sameFamilyMappings, - indexName, - }), - ].filter((x) => x !== ''); -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/index.tsx deleted file mode 100644 index 8d91e26a0da09..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; - -import { SameFamilyCallout } from '../callouts/same_family_callout'; -import { CompareFieldsTable } from '../compare_fields_table'; -import { getIncompatibleMappingsTableColumns } from '../compare_fields_table/get_incompatible_mappings_table_columns'; -import { useDataQualityContext } from '../../../../../../../../data_quality_context'; -import { getAllSameFamilyMarkdownComments, getSameFamilyMappings } from './helpers'; -import { SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE } from './translations'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../../../../../../types'; -import { StickyActions } from '../sticky_actions'; - -interface Props { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -} - -const SameFamilyTabComponent: React.FC = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}) => { - const sameFamilyMappings = useMemo( - () => getSameFamilyMappings(partitionedFieldMetadata.sameFamily), - [partitionedFieldMetadata.sameFamily] - ); - - const { isILMAvailable } = useDataQualityContext(); - const markdownComment: string = useMemo( - () => - getAllSameFamilyMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }).join('\n'), - [ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - ] - ); - - return ( -
- - - <> - {sameFamilyMappings.length > 0 && ( - <> - - - - - )} - - - 0 ? 'm' : 'l'} /> - -
- ); -}; - -SameFamilyTabComponent.displayName = 'SameFamilyTabComponent'; - -export const SameFamilyTab = React.memo(SameFamilyTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/translations.ts deleted file mode 100644 index c31afe1637bf6..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/translations.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle', - { - values: { indexName }, - defaultMessage: 'Same family field mappings - {indexName}', - } - ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles.tsx deleted file mode 100644 index 20976b1684003..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; - -export const DEFAULT_LEGEND_HEIGHT = 300; // px -export const DEFAULT_MAX_CHART_HEIGHT = 300; // px - -export const CalloutItem = styled.div` - margin-left: ${({ theme }) => theme.eui.euiSizeS}; -`; - -export const ChartFlexItem = styled(EuiFlexItem)<{ - $maxChartHeight: number | undefined; - $minChartHeight: number; -}>` - ${({ $maxChartHeight }) => ($maxChartHeight != null ? `max-height: ${$maxChartHeight}px;` : '')} - min-height: ${({ $minChartHeight }) => `${$minChartHeight}px`}; -`; - -export const LegendContainer = styled.div<{ - $height?: number; - $width?: number; -}>` - margin-left: ${({ theme }) => theme.eui.euiSizeM}; - margin-top: ${({ theme }) => theme.eui.euiSizeM}; - ${({ $height }) => ($height != null ? `height: ${$height}px;` : '')} - scrollbar-width: thin; - ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} -`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.test.ts deleted file mode 100644 index ba62b5b0f7874..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import numeral from '@elastic/numeral'; - -import { - eventCategory, - mockCustomFields, - mockIncompatibleMappings, - sourceIpWithTextMapping, -} from '../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { EMPTY_STAT } from '../../../../../../../constants'; -import { mockPartitionedFieldMetadata } from '../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { - escapePreserveNewlines, - getAllowedValues, - getCustomMarkdownTableRows, - getIncompatibleMappingsMarkdownTableRows, - getIncompatibleValuesMarkdownTableRows, - getIndexInvalidValues, - getMarkdownComment, - getMarkdownTable, - getSameFamilyBadge, - getSummaryMarkdownComment, - getSummaryTableMarkdownComment, - getTabCountsMarkdownComment, -} from './markdown'; -import { - ECS_MAPPING_TYPE_EXPECTED, - FIELD, - INDEX_MAPPING_TYPE_ACTUAL, -} from '../tabs/compare_fields_table/translations'; -import { mockAllowedValues } from '../../../../../../../mock/allowed_values/mock_allowed_values'; -import { EcsBasedFieldMetadata, UnallowedValueCount } from '../../../../../../../types'; -import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../tabs/incompatible_tab/translations'; -import { escapeNewlines } from '../../../../../../../utils/markdown'; -import { SAME_FAMILY_BADGE_LABEL } from '../translate'; - -const defaultBytesFormat = '0,0.[0]b'; -const formatBytes = (value: number | undefined) => - value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; - -const defaultNumberFormat = '0,0.[000]'; -const formatNumber = (value: number | undefined) => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; - -const indexName = 'auditbeat-custom-index-1'; - -describe('getSummaryTableMarkdownComment', () => { - test('it returns the expected comment', () => { - expect( - getSummaryTableMarkdownComment({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual( - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n' - ); - }); - - test('it returns the expected comment when isILMAvailable is false', () => { - expect( - getSummaryTableMarkdownComment({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: undefined, - }) - ).toEqual( - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n' - ); - }); - - test('it returns the expected comment when sizeInBytes is undefined', () => { - expect( - getSummaryTableMarkdownComment({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable: false, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - patternDocsCount: 57410, - sizeInBytes: undefined, - }) - ).toEqual( - '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n' - ); - }); -}); - -describe('escapeNewlines', () => { - test('it returns undefined when `content` is undefined', () => { - expect(escapeNewlines(undefined)).toBeUndefined(); - }); - - test("it returns the content unmodified when there's nothing to escape", () => { - const content = "there's nothing to escape in this content"; - expect(escapeNewlines(content)).toEqual(content); - }); - - test('it replaces all newlines in the content with spaces', () => { - const content = '\nthere were newlines in the beginning, middle,\nand end\n'; - expect(escapeNewlines(content)).toEqual( - ' there were newlines in the beginning, middle, and end ' - ); - }); - - test('it escapes all column separators in the content with spaces', () => { - const content = '|there were column separators in the beginning, middle,|and end|'; - expect(escapeNewlines(content)).toEqual( - '\\|there were column separators in the beginning, middle,\\|and end\\|' - ); - }); - - test('it escapes content containing BOTH newlines and column separators', () => { - const content = - '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; - expect(escapeNewlines(content)).toEqual( - '\\| there were newlines and column separators in the beginning, middle, \\|and end\\| ' - ); - }); -}); - -describe('escapePreserveNewlines', () => { - test('it returns undefined when `content` is undefined', () => { - expect(escapePreserveNewlines(undefined)).toBeUndefined(); - }); - - test("it returns the content unmodified when there's nothing to escape", () => { - const content = "there's (also) nothing to escape in this content"; - expect(escapePreserveNewlines(content)).toEqual(content); - }); - - test('it escapes all column separators in the content with spaces', () => { - const content = '|there were column separators in the beginning, middle,|and end|'; - expect(escapePreserveNewlines(content)).toEqual( - '\\|there were column separators in the beginning, middle,\\|and end\\|' - ); - }); - - test('it does NOT escape newlines in the content', () => { - const content = - '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; - expect(escapePreserveNewlines(content)).toEqual( - '\\|\nthere were newlines and column separators in the beginning, middle,\n\\|and end\\|\n' - ); - }); -}); - -describe('getAllowedValues', () => { - test('it returns the expected placeholder when `allowedValues` is undefined', () => { - expect(getAllowedValues(undefined)).toEqual('`--`'); - }); - - test('it joins the `allowedValues` `name`s as a markdown-code-formatted, comma separated, string', () => { - expect(getAllowedValues(mockAllowedValues)).toEqual( - '`authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web`' - ); - }); -}); - -describe('getIndexInvalidValues', () => { - test('it returns the expected placeholder when `indexInvalidValues` is empty', () => { - expect(getIndexInvalidValues([])).toEqual('`--`'); - }); - - test('it returns markdown-code-formatted `fieldName`s, and their associated `count`s', () => { - const indexInvalidValues: UnallowedValueCount[] = [ - { - count: 2, - fieldName: 'an_invalid_category', - }, - { - count: 1, - fieldName: 'theory', - }, - ]; - - expect(getIndexInvalidValues(indexInvalidValues)).toEqual( - `\`an_invalid_category\` (2), \`theory\` (1)` - ); - }); -}); - -describe('getCustomMarkdownTableRows', () => { - test('it returns the expected table rows', () => { - expect(getCustomMarkdownTableRows(mockCustomFields)).toEqual( - '| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |' - ); - }); -}); - -describe('getSameFamilyBadge', () => { - test('it returns the expected badge text when the field is in the same family', () => { - const inSameFamily = { - ...eventCategory, - isInSameFamily: true, - }; - - expect(getSameFamilyBadge(inSameFamily)).toEqual(`\`${SAME_FAMILY_BADGE_LABEL}\``); - }); - - test('it returns an empty string when the field is NOT the same family', () => { - const notInSameFamily = { - ...eventCategory, - isInSameFamily: false, - }; - - expect(getSameFamilyBadge(notInSameFamily)).toEqual(''); - }); -}); - -describe('getIncompatibleMappingsMarkdownTableRows', () => { - test('it returns the expected table rows when the field is in the same family', () => { - const eventCategoryWithWildcard: EcsBasedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType: 'wildcard', // this index has a mapping of `wildcard` instead of `keyword` - isInSameFamily: true, // `wildcard` and `keyword` are in the same family - }; - - expect( - getIncompatibleMappingsMarkdownTableRows([eventCategoryWithWildcard, sourceIpWithTextMapping]) - ).toEqual( - '| event.category | `keyword` | `wildcard` `same family` |\n| source.ip | `ip` | `text` |' - ); - }); - - test('it returns the expected table rows when the field is NOT in the same family', () => { - const eventCategoryWithText: EcsBasedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType: 'text', // this index has a mapping of `text` instead of `keyword` - isInSameFamily: false, // `text` and `keyword` are NOT in the same family - }; - - expect( - getIncompatibleMappingsMarkdownTableRows([eventCategoryWithText, sourceIpWithTextMapping]) - ).toEqual('| event.category | `keyword` | `text` |\n| source.ip | `ip` | `text` |'); - }); -}); - -describe('getIncompatibleValuesMarkdownTableRows', () => { - test('it returns the expected table rows', () => { - expect( - getIncompatibleValuesMarkdownTableRows([ - { - ...eventCategory, - hasEcsMetadata: true, - indexInvalidValues: [ - { - count: 2, - fieldName: 'an_invalid_category', - }, - { - count: 1, - fieldName: 'theory', - }, - ], - isEcsCompliant: false, - }, - ]) - ).toEqual( - '| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |' - ); - }); -}); - -describe('getMarkdownComment', () => { - test('it returns the expected markdown comment', () => { - const suggestedAction = - '|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n|and end|\n'; - const title = - '|\nthere were newlines and column separators in this title beginning, middle,\n|and end|\n'; - - expect(getMarkdownComment({ suggestedAction, title })).toEqual( - '#### \\| there were newlines and column separators in this title beginning, middle, \\|and end\\| \n\n\\|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n\\|and end\\|\n' - ); - }); -}); - -describe('getMarkdownTable', () => { - test('it returns the expected table contents', () => { - expect( - getMarkdownTable({ - enrichedFieldMetadata: mockIncompatibleMappings, - getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, - headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], - title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), - }) - ).toEqual( - '#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n' - ); - }); - - test('it returns an empty string when `enrichedFieldMetadata` is empty', () => { - expect( - getMarkdownTable({ - enrichedFieldMetadata: [], // <-- empty - getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, - headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], - title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), - }) - ).toEqual(''); - }); -}); - -describe('getSummaryMarkdownComment', () => { - test('it returns the expected markdown comment', () => { - expect(getSummaryMarkdownComment(indexName)).toEqual('### auditbeat-custom-index-1\n'); - }); -}); - -describe('getTabCountsMarkdownComment', () => { - test('it returns a comment with the expected counts', () => { - expect(getTabCountsMarkdownComment(mockPartitionedFieldMetadata)).toBe( - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n' - ); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.ts deleted file mode 100644 index 574408cfe600b..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/utils/markdown.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { INCOMPATIBLE_FIELDS } from '../../../../../../../translations'; -import { - escapeNewlines, - getCodeFormattedValue, - getMarkdownTableHeader, - getSummaryTableMarkdownHeader, - getSummaryTableMarkdownRow, -} from '../../../../../../../utils/markdown'; -import { - AllowedValue, - CustomFieldMetadata, - EcsBasedFieldMetadata, - EnrichedFieldMetadata, - IlmPhase, - PartitionedFieldMetadata, - UnallowedValueCount, -} from '../../../../../../../types'; -import { ALL_FIELDS, CUSTOM_FIELDS, ECS_COMPLIANT_FIELDS, SAME_FAMILY } from '../../translations'; -import { SAME_FAMILY_BADGE_LABEL } from '../translate'; - -export const getSummaryTableMarkdownComment = ({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, -}: { - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - isILMAvailable: boolean; - partitionedFieldMetadata: PartitionedFieldMetadata; - patternDocsCount: number; - sizeInBytes: number | undefined; -}): string => - `${getSummaryTableMarkdownHeader(isILMAvailable)} -${getSummaryTableMarkdownRow({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - incompatible: partitionedFieldMetadata.incompatible.length, - patternDocsCount, - sizeInBytes, -})} -`; - -export const escapePreserveNewlines = (content: string | undefined): string | undefined => - content != null ? content.replaceAll('|', '\\|') : content; - -export const getAllowedValues = (allowedValues: AllowedValue[] | undefined): string => - allowedValues == null - ? getCodeFormattedValue(undefined) - : allowedValues.map((x) => getCodeFormattedValue(x.name)).join(', '); - -export const getIndexInvalidValues = (indexInvalidValues: UnallowedValueCount[]): string => - indexInvalidValues.length === 0 - ? getCodeFormattedValue(undefined) - : indexInvalidValues - .map( - ({ fieldName, count }) => `${getCodeFormattedValue(escapeNewlines(fieldName))} (${count})` - ) - .join(', '); // newlines are instead joined with spaces - -export const getCustomMarkdownTableRows = (customFieldMetadata: CustomFieldMetadata[]): string => - customFieldMetadata - .map( - (x) => - `| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue( - x.indexFieldType - )} | ${getAllowedValues(undefined)} |` - ) - .join('\n'); - -export const getSameFamilyBadge = (ecsBasedFieldMetadata: EcsBasedFieldMetadata): string => - ecsBasedFieldMetadata.isInSameFamily ? getCodeFormattedValue(SAME_FAMILY_BADGE_LABEL) : ''; - -export const getIncompatibleMappingsMarkdownTableRows = ( - incompatibleMappings: EcsBasedFieldMetadata[] -): string => - incompatibleMappings - .map( - (x) => - `| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue( - x.type - )} | ${getCodeFormattedValue(x.indexFieldType)} ${getSameFamilyBadge(x)} |` - ) - .join('\n'); - -export const getIncompatibleValuesMarkdownTableRows = ( - incompatibleValues: EcsBasedFieldMetadata[] -): string => - incompatibleValues - .map( - (x) => - `| ${escapeNewlines(x.indexFieldName)} | ${getAllowedValues( - x.allowed_values - )} | ${getIndexInvalidValues(x.indexInvalidValues)} |` - ) - .join('\n'); - -export const getMarkdownComment = ({ - suggestedAction, - title, -}: { - suggestedAction: string; - title: string; -}): string => - `#### ${escapeNewlines(title)} - -${escapePreserveNewlines(suggestedAction)}`; - -export const getMarkdownTable = ({ - enrichedFieldMetadata, - getMarkdownTableRows, - headerNames, - title, -}: { - enrichedFieldMetadata: T; - getMarkdownTableRows: (enrichedFieldMetadata: T) => string; - headerNames: string[]; - title: string; -}): string => - enrichedFieldMetadata.length > 0 - ? `#### ${escapeNewlines(title)} - -${getMarkdownTableHeader(headerNames)} -${getMarkdownTableRows(enrichedFieldMetadata)} -` - : ''; - -export const getSummaryMarkdownComment = (indexName: string) => - `### ${escapeNewlines(indexName)} -`; - -export const getTabCountsMarkdownComment = ( - partitionedFieldMetadata: PartitionedFieldMetadata -): string => - `### **${INCOMPATIBLE_FIELDS}** ${getCodeFormattedValue( - `${partitionedFieldMetadata.incompatible.length}` - )} **${SAME_FAMILY}** ${getCodeFormattedValue( - `${partitionedFieldMetadata.sameFamily.length}` - )} **${CUSTOM_FIELDS}** ${getCodeFormattedValue( - `${partitionedFieldMetadata.custom.length}` - )} **${ECS_COMPLIANT_FIELDS}** ${getCodeFormattedValue( - `${partitionedFieldMetadata.ecsCompliant.length}` - )} **${ALL_FIELDS}** ${getCodeFormattedValue(`${partitionedFieldMetadata.all.length}`)} -`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.test.tsx deleted file mode 100644 index b589eb09e8d12..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { screen, render } from '@testing-library/react'; - -import { IndexStatsPanel } from '.'; -import { TestExternalProviders } from '../../../../../../mock/test_providers/test_providers'; - -describe('IndexStatsPanel', () => { - it('renders stats panel', () => { - render( - - - - ); - - const container = screen.getByTestId('indexStatsPanel'); - - expect(container).toHaveTextContent('Docs123'); - expect(container).toHaveTextContent('ILM phasehot'); - expect(container).toHaveTextContent('Size789'); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.tsx deleted file mode 100644 index 2df01db28f8a2..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_stats_panel/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { DOCS, ILM_PHASE, SIZE } from '../../../../../../translations'; -import { Stat } from '../../../../../../stat'; -import { getIlmPhaseDescription } from '../../../../../../utils/get_ilm_phase_description'; - -const StyledFlexItem = styled(EuiFlexItem)` - border-right: 1px solid ${({ theme }) => theme.eui.euiBorderColor}; - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - - &:last-child { - border-right: none; - } - - strong { - text-transform: capitalize; - } -`; - -export interface Props { - docsCount: string; - ilmPhase: string; - sizeInBytes: string; -} - -export const IndexStatsPanelComponent: React.FC = ({ docsCount, ilmPhase, sizeInBytes }) => ( - - - - {DOCS} - - {docsCount} - - -
- {ILM_PHASE} - - -
-
- - {SIZE} - - {sizeInBytes} - -
-
-); - -IndexStatsPanelComponent.displayName = 'IndexStatsPanelComponent'; - -export const IndexStatsPanel = React.memo(IndexStatsPanelComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/translations.ts deleted file mode 100644 index 402b58c4915db..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/translations.ts +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ADD_TO_NEW_CASE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton', - { - defaultMessage: 'Add to new case', - } -); - -export const ALL_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allFieldsLabel', - { - defaultMessage: 'All fields', - } -); - -export const ALL_CALLOUT = (version: string) => - i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCallout', { - values: { version }, - defaultMessage: - "All mappings for the fields in this index, including fields that comply with the Elastic Common Schema (ECS), version {version}, and fields that don't", - }); - -export const ALL_CALLOUT_TITLE = (fieldCount: number) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutTitle', - { - values: { fieldCount }, - defaultMessage: - 'All {fieldCount} {fieldCount, plural, =1 {field mapping} other {field mappings}}', - } - ); - -export const ALL_EMPTY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyContent', - { - defaultMessage: 'This index does not contain any mappings', - } -); - -export const ALL_EMPTY_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyTitle', - { - defaultMessage: 'No mappings', - } -); - -export const ALL_FIELDS_TABLE_TITLE = (indexName: string) => - i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.allTab.allFieldsTableTitle', { - values: { indexName }, - defaultMessage: 'All fields - {indexName}', - }); - -export const SUMMARY_MARKDOWN_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle', - { - defaultMessage: 'Data quality', - } -); - -export const SUMMARY_MARKDOWN_DESCRIPTION = ({ - ecsFieldReferenceUrl, - ecsReferenceUrl, - indexName, - mappingUrl, - version, -}: { - ecsFieldReferenceUrl: string; - ecsReferenceUrl: string; - indexName: string; - mappingUrl: string; - version: string; -}) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription', - { - values: { ecsFieldReferenceUrl, ecsReferenceUrl, indexName, mappingUrl, version }, - defaultMessage: - 'The `{indexName}` index has [mappings]({mappingUrl}) or field values that are different than the [Elastic Common Schema]({ecsReferenceUrl}) (ECS), version `{version}` [definitions]({ecsFieldReferenceUrl}).', - } - ); - -export const COPY_TO_CLIPBOARD = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.copyToClipboardButton', - { - defaultMessage: 'Copy to clipboard', - } -); - -export const CUSTOM_FIELDS_TABLE_TITLE = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.customTab.customFieldsTableTitle', - { - values: { indexName }, - defaultMessage: 'Custom fields - {indexName}', - } - ); - -export const CUSTOM_DETECTION_ENGINE_RULES_WORK = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage', - { - defaultMessage: '✅ Custom detection engine rules work', - } -); - -export const ECS_COMPLIANT_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantFieldsLabel', - { - defaultMessage: 'ECS compliant fields', - } -); - -export const ECS_COMPLIANT_CALLOUT = ({ - fieldCount, - version, -}: { - fieldCount: number; - version: string; -}) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout', - { - values: { fieldCount, version }, - defaultMessage: - 'The {fieldCount, plural, =1 {index mapping type and document values for this field comply} other {index mapping types and document values of these fields comply}} with the Elastic Common Schema (ECS), version {version}', - } - ); - -export const ECS_COMPLIANT_CALLOUT_TITLE = (fieldCount: number) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle', - { - values: { fieldCount }, - defaultMessage: '{fieldCount} ECS compliant {fieldCount, plural, =1 {field} other {fields}}', - } - ); - -export const ECS_COMPLIANT_EMPTY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent', - { - defaultMessage: - 'None of the field mappings in this index comply with the Elastic Common Schema (ECS). The index must (at least) contain an @timestamp date field.', - } -); - -export const ECS_VERSION_MARKDOWN_COMMENT = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsVersionMarkdownComment', - { - defaultMessage: 'Elastic Common Schema (ECS) version', - } -); - -export const INDEX = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown', - { - defaultMessage: 'Index', - } -); - -export const ECS_COMPLIANT_EMPTY_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyTitle', - { - defaultMessage: 'No ECS compliant Mappings', - } -); - -export const ECS_COMPLIANT_MAPPINGS_ARE_FULLY_SUPPORTED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantMappingsAreFullySupportedMessage', - { - defaultMessage: '✅ ECS compliant mappings and field values are fully supported', - } -); - -export const ERROR_LOADING_MAPPINGS_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle', - { - defaultMessage: 'Unable to load index mappings', - } -); - -export const ERROR_LOADING_MAPPINGS_BODY = (error: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody', - { - values: { error }, - defaultMessage: 'There was a problem loading mappings: {error}', - } - ); - -export const ERROR_LOADING_UNALLOWED_VALUES_BODY = (error: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesBody', - { - values: { error }, - defaultMessage: 'There was a problem loading unallowed values: {error}', - } - ); - -export const ERROR_LOADING_UNALLOWED_VALUES_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesTitle', - { - defaultMessage: 'Unable to load unallowed values', - } -); - -export const ERROR_GENERIC_CHECK_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorGenericCheckTitle', - { - defaultMessage: 'An error occurred during the check', - } -); - -export const ECS_COMPLIANT_FIELDS_TABLE_TITLE = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.customTab.ecsComplaintFieldsTableTitle', - { - values: { indexName }, - defaultMessage: 'ECS complaint fields - {indexName}', - } - ); - -export const LOADING_MAPPINGS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingMappingsPrompt', - { - defaultMessage: 'Loading mappings', - } -); - -export const LOADING_UNALLOWED_VALUES = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingUnallowedValuesPrompt', - { - defaultMessage: 'Loading unallowed values', - } -); - -export const CHECKING_INDEX = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.checkingIndexPrompt', - { - defaultMessage: 'Checking index', - } -); - -export const MISSING_TIMESTAMP_CALLOUT = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout', - { - defaultMessage: - 'Consider adding an @timestamp (date) field mapping to this index, as required by the Elastic Common Schema (ECS), because:', - } -); - -export const MISSING_TIMESTAMP_CALLOUT_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCalloutTitle', - { - defaultMessage: 'Missing an @timestamp (date) field mapping for this index', - } -); - -export const CUSTOM_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customFieldsLabel', - { - defaultMessage: 'Custom fields', - } -); - -export const CUSTOM_CALLOUT = ({ fieldCount, version }: { fieldCount: number; version: string }) => - i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCallout', { - values: { fieldCount, version }, - defaultMessage: - '{fieldCount, plural, =1 {This field is not} other {These fields are not}} defined by the Elastic Common Schema (ECS), version {version}.', - }); - -export const SAME_FAMILY_CALLOUT = ({ - fieldCount, - version, -}: { - fieldCount: number; - version: string; -}) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCallout', - { - values: { fieldCount, version }, - defaultMessage: - "{fieldCount, plural, =1 {This field is} other {These fields are}} defined by the Elastic Common Schema (ECS), version {version}, but {fieldCount, plural, =1 {its mapping type doesn't} other {their mapping types don't}} exactly match.", - } - ); - -export const CUSTOM_CALLOUT_TITLE = (fieldCount: number) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCalloutTitle', - { - values: { fieldCount }, - defaultMessage: - '{fieldCount} Custom {fieldCount, plural, =1 {field mapping} other {field mappings}}', - } - ); - -export const SAME_FAMILY_CALLOUT_TITLE = (fieldCount: number) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCalloutTitle', - { - values: { fieldCount }, - defaultMessage: - '{fieldCount} Same family {fieldCount, plural, =1 {field mapping} other {field mappings}}', - } - ); - -export const CUSTOM_EMPTY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyContent', - { - defaultMessage: 'All the field mappings in this index are defined by the Elastic Common Schema', - } -); - -export const CUSTOM_EMPTY_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyTitle', - { - defaultMessage: 'All field mappings defined by ECS', - } -); - -export const INCOMPATIBLE_CALLOUT = (version: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout', - { - values: { version }, - defaultMessage: - "Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version {version}.", - } - ); - -export const FIELDS_WITH_MAPPINGS_SAME_FAMILY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.fieldsWithMappingsSameFamilyLabel', - { - defaultMessage: - 'Fields with mappings in the same family have exactly the same search behavior as the type specified by ECS, but may have different space usage or performance characteristics.', - } -); - -export const WHEN_A_FIELD_IS_INCOMPATIBLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.whenAFieldIsIncompatibleLabel', - { - defaultMessage: 'When a field is incompatible:', - } -); - -export const INCOMPATIBLE_CALLOUT_TITLE = (fieldCount: number) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle', - { - values: { fieldCount }, - defaultMessage: '{fieldCount} incompatible {fieldCount, plural, =1 {field} other {fields}}', - } - ); - -export const INCOMPATIBLE_EMPTY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent', - { - defaultMessage: - 'All of the field mappings and document values in this index are compliant with the Elastic Common Schema (ECS).', - } -); - -export const INCOMPATIBLE_EMPTY_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle', - { - defaultMessage: 'All field mappings and values are ECS compliant', - } -); - -export const DETECTION_ENGINE_RULES_WILL_WORK = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage', - { - defaultMessage: '✅ Detection engine rules will work for these fields', - } -); - -export const DETECTION_ENGINE_RULES_MAY_NOT_MATCH = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage', - { - defaultMessage: - '❌ Detection engine rules referencing these fields may not match them correctly', - } -); - -export const OTHER_APP_CAPABILITIES_WORK_PROPERLY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.otherAppCapabilitiesWorkProperlyMessage', - { - defaultMessage: '✅ Other app capabilities work properly', - } -); - -export const SAME_FAMILY_EMPTY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyContent', - { - defaultMessage: - 'All of the field mappings and document values in this index are compliant with the Elastic Common Schema (ECS).', - } -); - -export const SAME_FAMILY_EMPTY_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyTitle', - { - defaultMessage: 'All field mappings and values are ECS compliant', - } -); - -export const PAGES_DISPLAY_EVENTS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesDisplayEventsMessage', - { - defaultMessage: '✅ Pages display events and fields correctly', - } -); - -export const PAGES_MAY_NOT_DISPLAY_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayFieldsMessage', - { - defaultMessage: '🌕 Some pages and features may not display these fields', - } -); - -export const PAGES_MAY_NOT_DISPLAY_EVENTS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayEventsMessage', - { - defaultMessage: - '❌ Pages may not display some events or fields due to unexpected field mappings or values', - } -); - -export const PRE_BUILT_DETECTION_ENGINE_RULES_WORK = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.preBuiltDetectionEngineRulesWorkMessage', - { - defaultMessage: '✅ Pre-built detection engine rules work', - } -); - -export const ECS_IS_A_PERMISSIVE_SCHEMA = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsIsAPermissiveSchemaMessage', - { - defaultMessage: - 'ECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.', - } -); - -export const SAME_FAMILY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyTab', - { - defaultMessage: 'Same family', - } -); - -export const SOMETIMES_INDICES_CREATED_BY_OLDER = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription', - { - defaultMessage: - 'Sometimes, indices created by older integrations will have mappings or values that were, but are no longer compliant.', - } -); - -export const MAPPINGS_THAT_CONFLICT_WITH_ECS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage', - { - defaultMessage: "❌ Mappings or field values that don't comply with ECS are not supported", - } -); - -export const UNKNOWN = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel', - { - defaultMessage: 'Unknown', - } -); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts deleted file mode 100644 index 68f4414a624a0..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getIndexPropertiesContainerId } from './get_index_properties_container_id'; - -describe('getIndexPropertiesContainerId', () => { - const pattern = 'auditbeat-*'; - const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; - - test('it returns the expected id', () => { - expect(getIndexPropertiesContainerId({ indexName, pattern })).toEqual( - 'index-properties-container-auditbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001' - ); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.test.ts deleted file mode 100644 index 63388b15c9495..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getIsInSameFamily } from './get_is_in_same_family'; - -describe('getIsInSameFamily', () => { - test('it returns false when ecsExpectedType is undefined', () => { - expect(getIsInSameFamily({ ecsExpectedType: undefined, type: 'keyword' })).toBe(false); - }); - - const expectedFamilyMembers: { - [key: string]: string[]; - } = { - constant_keyword: ['keyword', 'wildcard'], // `keyword` and `wildcard` in the same family as `constant_keyword` - keyword: ['constant_keyword', 'wildcard'], - match_only_text: ['text'], - text: ['match_only_text'], - wildcard: ['keyword', 'constant_keyword'], - }; - - const ecsExpectedTypes = Object.keys(expectedFamilyMembers); - - ecsExpectedTypes.forEach((ecsExpectedType) => { - const otherMembersOfSameFamily = expectedFamilyMembers[ecsExpectedType]; - - otherMembersOfSameFamily.forEach((type) => - test(`it returns true for ecsExpectedType '${ecsExpectedType}' when given '${type}', a type in the same family`, () => { - expect(getIsInSameFamily({ ecsExpectedType, type })).toBe(true); - }) - ); - - test(`it returns false for ecsExpectedType '${ecsExpectedType}' when given 'date', a type NOT in the same family`, () => { - expect(getIsInSameFamily({ ecsExpectedType, type: 'date' })).toBe(false); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.ts deleted file mode 100644 index aa56bc472cbd0..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Per https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html#_core_datatypes - * - * ``` - * Field types are grouped by _family_. Types in the same family have exactly - * the same search behavior but may have different space usage or - * performance characteristics. - * - * Currently, there are two type families, `keyword` and `text`. Other type - * families have only a single field type. For example, the `boolean` type - * family consists of one field type: `boolean`. - * ``` - */ -export const fieldTypeFamilies: Record> = { - keyword: new Set(['keyword', 'constant_keyword', 'wildcard']), - text: new Set(['text', 'match_only_text']), -}; - -export const getIsInSameFamily = ({ - ecsExpectedType, - type, -}: { - ecsExpectedType: string | undefined; - type: string; -}): boolean => { - if (ecsExpectedType != null) { - const allFamilies = Object.values(fieldTypeFamilies); - - return allFamilies.reduce( - (acc, family) => (acc !== true ? family.has(ecsExpectedType) && family.has(type) : acc), - false - ); - } else { - return false; - } -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.test.tsx new file mode 100644 index 0000000000000..79227efbb7861 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; + +import { IndexStatsPanel } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../../mock/test_providers/test_providers'; + +describe('IndexStatsPanel', () => { + it('renders stats panel', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).toHaveTextContent('ILM phasehot'); + expect(container).toHaveTextContent('Size789'); + }); + + describe('when sizeInBytes is not provided', () => { + it('renders 0 for size', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).toHaveTextContent('ILM phasehot'); + expect(container).toHaveTextContent('Size0'); + }); + }); + + describe('when ilmPhase is not provided', () => { + it('does not render ILM phase', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).not.toHaveTextContent('ILM phase'); + expect(container).toHaveTextContent('Size789'); + }); + }); + + describe('when customFieldsCount is provided', () => { + it('renders custom fields count', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).toHaveTextContent('Size789'); + expect(container).toHaveTextContent('Custom fields456'); + }); + }); + + describe('when ecsCompliantFieldsCount is provided', () => { + it('renders ecs compliant fields count', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).toHaveTextContent('Size789'); + expect(container).toHaveTextContent('ECS compliant fields456'); + }); + }); + + describe('when allFieldsCount is provided', () => { + it('renders all fields count', () => { + render( + + + + + + ); + + const container = screen.getByTestId('indexStatsPanel'); + + expect(container).toHaveTextContent('Docs123'); + expect(container).toHaveTextContent('Size789'); + expect(container).toHaveTextContent('All fields456'); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.tsx new file mode 100644 index 0000000000000..7f8598839fd0c --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_stats_panel/index.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { + ALL_FIELDS, + CUSTOM_FIELDS, + DOCS, + ECS_COMPLIANT_FIELDS, + ILM_PHASE, + SIZE, +} from '../../../../../translations'; +import { Stat } from '../../../../../stat'; +import { getIlmPhaseDescription } from '../../../../../utils/get_ilm_phase_description'; + +const StyledFlexItem = styled(EuiFlexItem)` + justify-content: space-between; + border-right: 1px solid ${({ theme }) => theme.eui.euiBorderColor}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + + margin-bottom: 2px; + + &:last-child { + border-right: none; + } + + strong { + text-transform: capitalize; + } +`; + +const UnpaddedStyledFlexItem = styled(StyledFlexItem)` + margin-bottom: 0; +`; + +export interface Props { + docsCount: number; + ilmPhase?: string; + sizeInBytes?: number; + sameFamilyFieldsCount?: number; + ecsCompliantFieldsCount?: number; + customFieldsCount?: number; + allFieldsCount?: number; +} + +export const IndexStatsPanelComponent: React.FC = ({ + docsCount, + ilmPhase, + sizeInBytes, + sameFamilyFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, +}) => { + const { formatBytes, formatNumber } = useDataQualityContext(); + return ( + + + + {DOCS} + + {formatNumber(docsCount)} + + {ilmPhase && ( + + {ILM_PHASE} + + + + )} + + {SIZE} + + {formatBytes(sizeInBytes ?? 0)} + + {customFieldsCount != null && ( + + {CUSTOM_FIELDS} + + {formatNumber(customFieldsCount)} + + )} + {ecsCompliantFieldsCount != null && ( + + {ECS_COMPLIANT_FIELDS} + + {formatNumber(ecsCompliantFieldsCount)} + + )} + {allFieldsCount != null && ( + + {ALL_FIELDS} + + {formatNumber(allFieldsCount)} + + )} + + + ); +}; + +IndexStatsPanelComponent.displayName = 'IndexStatsPanelComponent'; + +export const IndexStatsPanel = React.memo(IndexStatsPanelComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.test.tsx similarity index 86% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.test.tsx index 2d361ec0ed34f..c8e04776e90bd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.test.tsx @@ -15,9 +15,9 @@ import { TestDataQualityProviders, TestExternalProviders, } from '../../../../../mock/test_providers/test_providers'; -import { LOADING_MAPPINGS, LOADING_UNALLOWED_VALUES } from './translations'; -import { IndexProperties, Props } from '.'; -import { getCheckState } from '../../../../../stub/get_check_state'; +import { LatestResults, Props } from '.'; +import { getCheckStateStub } from '../../../../../stub/get_check_state_stub'; +import { LOADING_MAPPINGS, LOADING_UNALLOWED_VALUES } from '../translations'; const indexName = 'auditbeat-custom-index-1'; const defaultBytesFormat = '0,0.[0]b'; @@ -28,18 +28,16 @@ const defaultNumberFormat = '0,0.[000]'; const formatNumber = (value: number | undefined) => value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; -const pattern = 'auditbeat-*'; const patternRollup = auditbeatWithAllResults; const defaultProps: Props = { - docsCount: auditbeatWithAllResults.docsCount ?? 0, - ilmPhase: 'hot', + stats: auditbeatWithAllResults.stats, + ilmExplain: auditbeatWithAllResults.ilmExplain, indexName: 'auditbeat-custom-index-1', - pattern, patternRollup, }; -describe('IndexProperties', () => { +describe('LatestResults', () => { test('it renders the tab content', async () => { render( @@ -50,11 +48,11 @@ describe('IndexProperties', () => { }} indicesCheckContextProps={{ checkState: { - ...getCheckState(indexName), + ...getCheckStateStub(indexName), }, }} > - + ); @@ -84,13 +82,13 @@ describe('IndexProperties', () => { }} indicesCheckContextProps={{ checkState: { - ...getCheckState(indexName, { + ...getCheckStateStub(indexName, { mappingsError: new Error(error), }), }, }} > - + ); @@ -125,13 +123,13 @@ describe('IndexProperties', () => { }} indicesCheckContextProps={{ checkState: { - ...getCheckState(indexName, { + ...getCheckStateStub(indexName, { unallowedValuesError: new Error(error), }), }, }} > - + ); @@ -161,13 +159,13 @@ describe('IndexProperties', () => { }} indicesCheckContextProps={{ checkState: { - ...getCheckState(indexName, { + ...getCheckStateStub(indexName, { isLoadingMappings: true, }), }, }} > - + ); @@ -195,13 +193,13 @@ describe('IndexProperties', () => { }} indicesCheckContextProps={{ checkState: { - ...getCheckState(indexName, { + ...getCheckStateStub(indexName, { isLoadingUnallowedValues: true, }), }, }} > - + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.tsx new file mode 100644 index 0000000000000..339f3252f2e65 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; +import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { getDocsCount, getSizeInBytes } from '../../../../../utils/stats'; +import { getIlmPhase } from '../../../../../utils/get_ilm_phase'; +import { ErrorEmptyPrompt } from '../../error_empty_prompt'; +import { LoadingEmptyPrompt } from '../../loading_empty_prompt'; +import type { MeteringStatsIndex, PatternRollup } from '../../../../../types'; +import { useIndicesCheckContext } from '../../../../../contexts/indices_check_context'; +import { LatestCheckFields } from './latest_check_fields'; +import { IndexStatsPanel } from '../index_stats_panel'; +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { + CHECKING_INDEX, + ERROR_GENERIC_CHECK_TITLE, + ERROR_LOADING_MAPPINGS_TITLE, + ERROR_LOADING_UNALLOWED_VALUES_TITLE, + LOADING_MAPPINGS, + LOADING_UNALLOWED_VALUES, +} from '../translations'; + +export interface Props { + ilmExplain: Record | null; + indexName: string; + patternRollup: PatternRollup | undefined; + stats: Record | null; +} + +const LatestResultsComponent: React.FC = ({ + indexName, + patternRollup, + stats, + ilmExplain, +}) => { + const { isILMAvailable } = useDataQualityContext(); + const docsCount = useMemo(() => getDocsCount({ stats, indexName }), [stats, indexName]); + const sizeInBytes = useMemo(() => getSizeInBytes({ stats, indexName }), [indexName, stats]); + const ilmPhase = useMemo(() => { + return isILMAvailable && ilmExplain != null + ? getIlmPhase(ilmExplain?.[indexName], isILMAvailable) + : undefined; + }, [ilmExplain, indexName, isILMAvailable]); + + const { checkState } = useIndicesCheckContext(); + const indexCheckState = checkState[indexName]; + const isChecking = indexCheckState?.isChecking ?? false; + const isLoadingMappings = indexCheckState?.isLoadingMappings ?? false; + const isLoadingUnallowedValues = indexCheckState?.isLoadingUnallowedValues ?? false; + const genericCheckError = indexCheckState?.genericError ?? null; + const mappingsError = indexCheckState?.mappingsError ?? null; + const unallowedValuesError = indexCheckState?.unallowedValuesError ?? null; + const isCheckComplete = indexCheckState?.isCheckComplete ?? false; + + if (mappingsError != null) { + return ; + } else if (unallowedValuesError != null) { + return ; + } else if (genericCheckError != null) { + return ; + } + + if (isLoadingMappings) { + return ; + } else if (isLoadingUnallowedValues) { + return ; + } else if (isChecking) { + return ; + } + + return isCheckComplete ? ( +
+ + + +
+ ) : null; +}; + +LatestResultsComponent.displayName = 'LatestResultsComponent'; + +export const LatestResults = React.memo(LatestResultsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/all_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/index.tsx similarity index 55% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/all_tab/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/index.tsx index 085a865917b42..1074119e175ea 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/all_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/index.tsx @@ -9,34 +9,39 @@ import { EcsVersion } from '@elastic/ecs'; import { EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { CompareFieldsTable } from '../compare_fields_table'; -import { getCommonTableColumns } from '../compare_fields_table/get_common_table_columns'; +import { CompareFieldsTable } from '../../../compare_fields_table'; import { EmptyPromptBody } from '../../../empty_prompt_body'; import { EmptyPromptTitle } from '../../../empty_prompt_title'; -import * as i18n from '../../../translations'; -import type { PartitionedFieldMetadata } from '../../../../../../../../types'; +import type { EnrichedFieldMetadata } from '../../../../../../../types'; +import { getAllTableColumns } from './utils/get_all_table_columns'; +import { + ALL_CALLOUT, + ALL_EMPTY, + ALL_EMPTY_TITLE, + ALL_FIELDS_TABLE_TITLE, +} from '../../../translations'; interface Props { indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; + allFields: EnrichedFieldMetadata[]; } -const AllTabComponent: React.FC = ({ indexName, partitionedFieldMetadata }) => { - const body = useMemo(() => , []); - const title = useMemo(() => , []); +const AllTabComponent: React.FC = ({ indexName, allFields }) => { + const body = useMemo(() => , []); + const title = useMemo(() => , []); return (
- {partitionedFieldMetadata.all.length > 0 ? ( + {allFields.length > 0 ? ( <> -

{i18n.ALL_CALLOUT(EcsVersion)}

+

{ALL_CALLOUT(EcsVersion)}

) : ( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.test.tsx similarity index 78% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.test.tsx index 5bcd6bb90e0b7..5b1f78208f398 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.test.tsx @@ -9,27 +9,30 @@ import { render, screen } from '@testing-library/react'; import { omit } from 'lodash/fp'; import React from 'react'; +import { getAllTableColumns } from './get_all_table_columns'; import { eventCategory, - someField, eventCategoryWithUnallowedValues, -} from '../../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; + hostNameWithTextMapping, + mockAgentTypeSameFamilyField, + someField, +} from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; import { DOCUMENT_VALUES_ACTUAL, - ECS_DESCRIPTION, ECS_MAPPING_TYPE_EXPECTED, ECS_VALUES_EXPECTED, FIELD, INDEX_MAPPING_TYPE_ACTUAL, -} from '../translations'; -import { EnrichedFieldMetadata } from '../../../../../../../../../types'; -import { EMPTY_PLACEHOLDER, getCommonTableColumns } from '.'; -import { SAME_FAMILY_BADGE_LABEL } from '../../../translate'; + SAME_FAMILY_BADGE_LABEL, +} from '../../../../../../../../translations'; +import { EnrichedFieldMetadata } from '../../../../../../../../types'; +import { EMPTY_PLACEHOLDER } from '../../../../../../../../constants'; +import { ECS_DESCRIPTION } from '../../../../translations'; -describe('getCommonTableColumns', () => { +describe('getAllTableColumns', () => { test('it returns the expected column configuration', () => { - expect(getCommonTableColumns().map((x) => omit('render', x))).toEqual([ + expect(getAllTableColumns().map((x) => omit('render', x))).toEqual([ { field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '15%' }, { field: 'type', @@ -71,7 +74,7 @@ describe('getCommonTableColumns', () => { describe('type column render()', () => { test('it renders the expected type', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const typeColumnRender = columns[1].render; const expected = 'keyword'; @@ -85,7 +88,7 @@ describe('getCommonTableColumns', () => { }); test('it renders an empty placeholder when type is undefined', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const typeColumnRender = columns[1].render; render( @@ -100,31 +103,23 @@ describe('getCommonTableColumns', () => { describe('indexFieldType column render()', () => { describe("when the index field type does NOT match the ECS type, but it's in the SAME family", () => { - const indexFieldType = 'wildcard'; - beforeEach(() => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const indexFieldTypeColumnRender = columns[2].render; - const withTypeMismatchSameFamily: EnrichedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType, // this index has a mapping of `wildcard` instead of `keyword` - isInSameFamily: true, // `wildcard` and `keyword` are in the same family - }; - render( {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender( - withTypeMismatchSameFamily.indexFieldType, - withTypeMismatchSameFamily + mockAgentTypeSameFamilyField.indexFieldType, + mockAgentTypeSameFamilyField )} ); }); test('it renders the index field with a "success" style', () => { - expect(screen.getByTestId('codeSuccess')).toHaveTextContent(indexFieldType); + expect(screen.getByTestId('codeSuccess')).toHaveTextContent('constant_keyword'); }); test('it renders the same family badge', () => { @@ -136,21 +131,15 @@ describe('getCommonTableColumns', () => { const indexFieldType = 'text'; test('it renders the expected type with danger styling', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const indexFieldTypeColumnRender = columns[2].render; - const withTypeMismatchDifferentFamily: EnrichedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType, // this index has a mapping of `text` instead of `keyword` - isInSameFamily: false, // `text` and `wildcard` are not in the same family - }; - render( {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender( - withTypeMismatchDifferentFamily.indexFieldType, - withTypeMismatchDifferentFamily + hostNameWithTextMapping.indexFieldType, + hostNameWithTextMapping )} ); @@ -161,7 +150,7 @@ describe('getCommonTableColumns', () => { describe('when the index field matches the ECS type', () => { test('it renders the expected type with success styling', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const indexFieldTypeColumnRender = columns[2].render; render( @@ -178,7 +167,7 @@ describe('getCommonTableColumns', () => { describe('allowed_values column render()', () => { test('it renders the expected allowed values when provided', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const allowedValuesolumnRender = columns[3].render; const expectedAllowedValuesNames = @@ -197,7 +186,7 @@ describe('getCommonTableColumns', () => { }); test('it renders a placeholder when allowed values is undefined', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const allowedValuesolumnRender = columns[3].render; const withUndefinedAllowedValues: EnrichedFieldMetadata = { @@ -218,7 +207,7 @@ describe('getCommonTableColumns', () => { describe('indexInvalidValues column render()', () => { test('it renders the expected index invalid values', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const indexInvalidValuesRender = columns[4].render; render( @@ -239,7 +228,7 @@ describe('getCommonTableColumns', () => { describe('description column render()', () => { test('it renders the expected description', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const descriptionolumnRender = columns[5].render; const expectedDescription = 'this is a test'; @@ -259,7 +248,7 @@ describe('getCommonTableColumns', () => { }); test('it renders a placeholder when description is undefined', () => { - const columns = getCommonTableColumns(); + const columns = getAllTableColumns(); const descriptionolumnRender = columns[5].render; render( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.tsx similarity index 68% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.tsx index 757294cc89c6d..386582e5a4fe9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/get_common_table_columns/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/all_tab/utils/get_all_table_columns.tsx @@ -9,33 +9,41 @@ import type { EuiTableFieldDataColumnType } from '@elastic/eui'; import { EuiCode } from '@elastic/eui'; import React from 'react'; -import { SameFamily } from '../same_family'; -import { EcsAllowedValues } from '../ecs_allowed_values'; -import { IndexInvalidValues } from '../index_invalid_values'; -import { CodeDanger, CodeSuccess } from '../../../../../../../../../styles'; -import * as i18n from '../translations'; +import { + isCustomFieldMetadata, + isEcsCompliantFieldMetadata, + isSameFamilyFieldMetadata, +} from '../../../../../../../../utils/metadata'; +import { EMPTY_PLACEHOLDER } from '../../../../../../../../constants'; +import { + DOCUMENT_VALUES_ACTUAL, + ECS_MAPPING_TYPE_EXPECTED, + ECS_VALUES_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, +} from '../../../../../../../../translations'; +import { EcsAllowedValues } from '../../../../ecs_allowed_values'; +import { IndexInvalidValues } from '../../../../index_invalid_values'; +import { CodeDanger, CodeSuccess } from '../../../../../../../../styles'; import type { AllowedValue, EnrichedFieldMetadata, UnallowedValueCount, -} from '../../../../../../../../../types'; -import { getIsInSameFamily } from '../../../../utils/get_is_in_same_family'; +} from '../../../../../../../../types'; +import { SameFamily } from '../../../../same_family'; +import { ECS_DESCRIPTION } from '../../../../translations'; -export const EMPTY_PLACEHOLDER = '--'; - -export const getCommonTableColumns = (): Array< - EuiTableFieldDataColumnType -> => [ +export const getAllTableColumns = (): Array> => [ { field: 'indexFieldName', - name: i18n.FIELD, + name: FIELD, sortable: true, truncateText: false, width: '15%', }, { field: 'type', - name: i18n.ECS_MAPPING_TYPE_EXPECTED, + name: ECS_MAPPING_TYPE_EXPECTED, render: (type: string | undefined) => ( {type != null ? type : EMPTY_PLACEHOLDER} @@ -47,15 +55,15 @@ export const getCommonTableColumns = (): Array< }, { field: 'indexFieldType', - name: i18n.INDEX_MAPPING_TYPE_ACTUAL, + name: INDEX_MAPPING_TYPE_ACTUAL, render: (_, x) => { // if custom field or ecs based field with mapping match - if (!x.hasEcsMetadata || x.indexFieldType === x.type) { + if (isCustomFieldMetadata(x) || isEcsCompliantFieldMetadata(x)) { return {x.indexFieldType}; } // mapping mismatch due to same family - if (getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType })) { + if (isSameFamilyFieldMetadata(x)) { return (
{x.indexFieldType} @@ -73,7 +81,7 @@ export const getCommonTableColumns = (): Array< }, { field: 'allowed_values', - name: i18n.ECS_VALUES_EXPECTED, + name: ECS_VALUES_EXPECTED, render: (allowedValues: AllowedValue[] | undefined) => ( ), @@ -83,7 +91,7 @@ export const getCommonTableColumns = (): Array< }, { field: 'indexInvalidValues', - name: i18n.DOCUMENT_VALUES_ACTUAL, + name: DOCUMENT_VALUES_ACTUAL, render: (indexInvalidValues: UnallowedValueCount[]) => ( ), @@ -93,7 +101,7 @@ export const getCommonTableColumns = (): Array< }, { field: 'description', - name: i18n.ECS_DESCRIPTION, + name: ECS_DESCRIPTION, render: (description: string | undefined) => description != null ? ( {description} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.test.tsx similarity index 76% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.test.tsx index b83f1fdc7ca60..483b1743e083c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.test.tsx @@ -9,19 +9,15 @@ import { EcsVersion } from '@elastic/ecs'; import { render, screen } from '@testing-library/react'; import React from 'react'; +import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; import { ECS_IS_A_PERMISSIVE_SCHEMA } from '../../../../translations'; -import { - hostNameKeyword, - someField, -} from '../../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; import { CustomCallout } from '.'; describe('CustomCallout', () => { beforeEach(() => { render( - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.tsx similarity index 61% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.tsx index 1d631fa15a371..e91ff6ab0ef87 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/custom_callout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/custom_callout/index.tsx @@ -6,25 +6,22 @@ */ import { EcsVersion } from '@elastic/ecs'; - import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React from 'react'; -import type { CustomFieldMetadata } from '../../../../../../../../../types'; - -import * as i18n from '../../../../translations'; +import { CUSTOM_CALLOUT, ECS_IS_A_PERMISSIVE_SCHEMA } from '../../../../translations'; interface Props { - customFieldMetadata: CustomFieldMetadata[]; + fieldCount: number; } -const CustomCalloutComponent: React.FC = ({ customFieldMetadata }) => { +const CustomCalloutComponent: React.FC = ({ fieldCount }) => { return (
- {i18n.CUSTOM_CALLOUT({ fieldCount: customFieldMetadata.length, version: EcsVersion })} + {CUSTOM_CALLOUT({ fieldCount, version: EcsVersion })}
-
{i18n.ECS_IS_A_PERMISSIVE_SCHEMA}
+
{ECS_IS_A_PERMISSIVE_SCHEMA}
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/index.tsx similarity index 53% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/index.tsx index 2d7437e8f0638..561a6e4c389d9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/custom_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/index.tsx @@ -8,39 +8,44 @@ import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { CustomCallout } from '../callouts/custom_callout'; -import { CompareFieldsTable } from '../compare_fields_table'; -import { getCustomTableColumns } from '../compare_fields_table/helpers'; +import { CustomCallout } from './custom_callout'; +import { CompareFieldsTable } from '../../../compare_fields_table'; import { EmptyPromptBody } from '../../../empty_prompt_body'; import { EmptyPromptTitle } from '../../../empty_prompt_title'; -import { getAllCustomMarkdownComments, showCustomCallout } from './helpers'; -import * as i18n from '../../../translations'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../../../../../../types'; -import { useDataQualityContext } from '../../../../../../../../data_quality_context'; +import { getAllCustomMarkdownComments } from './utils/markdown'; +import type { CustomFieldMetadata, IlmPhase } from '../../../../../../../types'; +import { useDataQualityContext } from '../../../../../../../data_quality_context'; import { StickyActions } from '../sticky_actions'; +import { getCustomTableColumns } from './utils/get_custom_table_columns'; +import { CUSTOM_EMPTY, CUSTOM_EMPTY_TITLE, CUSTOM_FIELDS_TABLE_TITLE } from '../../../translations'; interface Props { docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata; + customFields: CustomFieldMetadata[]; + incompatibleFieldsCount: number; + sameFamilyFieldsCount: number; + ecsCompliantFieldsCount: number; + allFieldsCount: number; patternDocsCount: number; sizeInBytes: number | undefined; } const CustomTabComponent: React.FC = ({ docsCount, - formatBytes, - formatNumber, ilmPhase, indexName, - partitionedFieldMetadata, + customFields, + incompatibleFieldsCount, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, patternDocsCount, sizeInBytes, }) => { - const { isILMAvailable } = useDataQualityContext(); + const { formatBytes, formatNumber, isILMAvailable } = useDataQualityContext(); + const customFieldsCount = customFields.length; const markdownComment: string = useMemo( () => getAllCustomMarkdownComments({ @@ -50,38 +55,46 @@ const CustomTabComponent: React.FC = ({ ilmPhase, indexName, isILMAvailable, - partitionedFieldMetadata, + customFields, + incompatibleFieldsCount, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, patternDocsCount, sizeInBytes, }).join('\n'), [ + allFieldsCount, + customFields, docsCount, + ecsCompliantFieldsCount, formatBytes, formatNumber, ilmPhase, + incompatibleFieldsCount, indexName, isILMAvailable, - partitionedFieldMetadata, patternDocsCount, + sameFamilyFieldsCount, sizeInBytes, ] ); - const body = useMemo(() => , []); - const title = useMemo(() => , []); + const body = useMemo(() => , []); + const title = useMemo(() => , []); return (
- {showCustomCallout(partitionedFieldMetadata.custom) ? ( + {customFieldsCount > 0 ? ( <> - + diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/translate.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/translations.ts similarity index 64% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/translate.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/translations.ts index 1a94c062f0079..c691c507d789e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/translate.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/translations.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const SAME_FAMILY_BADGE_LABEL = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel', +export const INDEX_MAPPING_TYPE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexMappingType', { - defaultMessage: 'same family', + defaultMessage: 'Index mapping type', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.test.tsx new file mode 100644 index 0000000000000..7189b3043007b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { omit } from 'lodash/fp'; +import { getCustomTableColumns } from './get_custom_table_columns'; +import { render, screen } from '@testing-library/react'; + +import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; +import { someField } from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; + +describe('getCustomTableColumns', () => { + test('it returns the expected columns', () => { + expect(getCustomTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '50%', + }, + { + field: 'indexFieldType', + name: 'Index mapping type', + sortable: true, + truncateText: false, + width: '50%', + }, + ]); + }); + + describe('indexFieldType render()', () => { + test('it renders the indexFieldType', () => { + const columns = getCustomTableColumns(); + const indexFieldTypeRender = columns[1].render; + + render( + + <> + {indexFieldTypeRender != null && + indexFieldTypeRender(someField.indexFieldType, someField)} + + + ); + + expect(screen.getByTestId('indexFieldType')).toHaveTextContent(someField.indexFieldType); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.tsx new file mode 100644 index 0000000000000..ddd5bfb8f54a6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/get_custom_table_columns.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCode, EuiTableFieldDataColumnType } from '@elastic/eui'; +import React from 'react'; + +import { FIELD } from '../../../../../../../../translations'; +import { CustomFieldMetadata } from '../../../../../../../../types'; +import { INDEX_MAPPING_TYPE } from '../translations'; + +export const getCustomTableColumns = (): Array< + EuiTableFieldDataColumnType +> => [ + { + field: 'indexFieldName', + name: FIELD, + sortable: true, + truncateText: false, + width: '50%', + }, + { + field: 'indexFieldType', + name: INDEX_MAPPING_TYPE, + render: (indexFieldType: string) => ( + {indexFieldType} + ), + sortable: true, + truncateText: false, + width: '50%', + }, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.test.ts new file mode 100644 index 0000000000000..653c9d603975a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import numeral from '@elastic/numeral'; +import { EcsVersion } from '@elastic/ecs'; + +import { + getAllCustomMarkdownComments, + getCustomMarkdownComment, + getCustomMarkdownTableRows, +} from './markdown'; +import { mockCustomFields } from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { EMPTY_STAT } from '../../../../../../../../constants'; +import { ECS_IS_A_PERMISSIVE_SCHEMA } from '../../../../translations'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +describe('getCustomMarkdownComment', () => { + test('it returns a comment for custom fields with the expected field counts and ECS version', () => { + expect(getCustomMarkdownComment({ customFieldsCount: 2 })).toEqual(`#### 2 Custom field mappings + +These fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}. + +${ECS_IS_A_PERMISSIVE_SCHEMA} +`); + }); +}); + +describe('getCustomMarkdownTableRows', () => { + test('it returns the expected table rows', () => { + expect(getCustomMarkdownTableRows(mockCustomFields)).toEqual( + '| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |' + ); + }); +}); + +describe('getAllCustomMarkdownComments', () => { + test('it returns the expected comment', () => { + expect( + getAllCustomMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: true, + customFields: mockCustomFields, + incompatibleFieldsCount: 3, + ecsCompliantFieldsCount: 2, + sameFamilyFieldsCount: 0, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, + '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', + ]); + }); + + test('it returns the expected comment without ILM Phase when isILMAvailable is false', () => { + expect( + getAllCustomMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + customFields: mockCustomFields, + incompatibleFieldsCount: 3, + ecsCompliantFieldsCount: 2, + sameFamilyFieldsCount: 0, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, + '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', + ]); + }); + + test('it returns the expected comment without Size when Size is undefined', () => { + expect( + getAllCustomMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + customFields: mockCustomFields, + incompatibleFieldsCount: 3, + ecsCompliantFieldsCount: 2, + sameFamilyFieldsCount: 0, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: undefined, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, + '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', + ]); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.ts new file mode 100644 index 0000000000000..e830d22ce65b1 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/custom_tab/utils/markdown.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsVersion } from '@elastic/ecs'; + +import { CustomFieldMetadata, IlmPhase } from '../../../../../../../../types'; +import { FIELD } from '../../../../../../../../translations'; +import { + escapeNewlines, + getAllowedValues, + getCodeFormattedValue, + getMarkdownComment, + getMarkdownTable, + getSummaryMarkdownComment, + getSummaryTableMarkdownComment, + getTabCountsMarkdownComment, +} from '../../../../../../../../utils/markdown'; +import { INDEX_MAPPING_TYPE } from '../translations'; +import { + CUSTOM_CALLOUT, + CUSTOM_CALLOUT_TITLE, + CUSTOM_FIELDS_TABLE_TITLE, + ECS_IS_A_PERMISSIVE_SCHEMA, +} from '../../../../translations'; + +export const getCustomMarkdownComment = ({ + customFieldsCount, +}: { + customFieldsCount: number; +}): string => + getMarkdownComment({ + suggestedAction: `${CUSTOM_CALLOUT({ + fieldCount: customFieldsCount, + version: EcsVersion, + })} + +${ECS_IS_A_PERMISSIVE_SCHEMA} +`, + title: CUSTOM_CALLOUT_TITLE(customFieldsCount), + }); + +export const getCustomMarkdownTableRows = (customFieldMetadata: CustomFieldMetadata[]): string => + customFieldMetadata + .map( + (x) => + `| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue( + x.indexFieldType + )} | ${getAllowedValues(undefined)} |` + ) + .join('\n'); + +export const getAllCustomMarkdownComments = ({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + customFields, + incompatibleFieldsCount, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, +}: { + docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + ilmPhase: IlmPhase | undefined; + isILMAvailable: boolean; + indexName: string; + customFields: CustomFieldMetadata[]; + incompatibleFieldsCount: number; + sameFamilyFieldsCount: number; + ecsCompliantFieldsCount: number; + allFieldsCount: number; + patternDocsCount: number; + sizeInBytes: number | undefined; +}): string[] => { + const customFieldsCount = customFields.length; + return [ + getSummaryMarkdownComment(indexName), + getSummaryTableMarkdownComment({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleFieldsCount, + patternDocsCount, + sizeInBytes, + }), + getTabCountsMarkdownComment({ + allFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + incompatibleFieldsCount, + sameFamilyFieldsCount, + }), + getCustomMarkdownComment({ + customFieldsCount, + }), + getMarkdownTable({ + enrichedFieldMetadata: customFields, + getMarkdownTableRows: getCustomMarkdownTableRows, + headerNames: [FIELD, INDEX_MAPPING_TYPE], + title: CUSTOM_FIELDS_TABLE_TITLE(indexName), + }), + ]; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/index.tsx new file mode 100644 index 0000000000000..e2ad16b73c647 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsVersion } from '@elastic/ecs'; + +import { EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { CompareFieldsTable } from '../../../compare_fields_table'; +import { EmptyPromptBody } from '../../../empty_prompt_body'; +import { EmptyPromptTitle } from '../../../empty_prompt_title'; +import type { EcsCompliantFieldMetadata } from '../../../../../../../types'; +import { isTimestampFieldMissing } from '../utils/is_timestamp_field_missing'; +import { getEcsCompliantTableColumns } from './utils/get_ecs_compliant_table_columns'; +import { CalloutItem } from '../../../styles'; +import { + CUSTOM_DETECTION_ENGINE_RULES_WORK, + ECS_COMPLIANT_CALLOUT, + ECS_COMPLIANT_EMPTY, + ECS_COMPLIANT_EMPTY_TITLE, + ECS_COMPLIANT_FIELDS_TABLE_TITLE, + ECS_COMPLIANT_MAPPINGS_ARE_FULLY_SUPPORTED, + OTHER_APP_CAPABILITIES_WORK_PROPERLY, + PAGES_DISPLAY_EVENTS, + PRE_BUILT_DETECTION_ENGINE_RULES_WORK, +} from '../../../translations'; + +const EmptyPromptContainer = styled.div` + width: 100%; +`; + +interface Props { + indexName: string; + ecsCompliantFields: EcsCompliantFieldMetadata[]; +} + +const EcsCompliantTabComponent: React.FC = ({ indexName, ecsCompliantFields }) => { + const emptyPromptBody = useMemo(() => , []); + const title = useMemo(() => , []); + + return ( +
+ {!isTimestampFieldMissing(ecsCompliantFields) ? ( + <> + +

+ {ECS_COMPLIANT_CALLOUT({ + fieldCount: ecsCompliantFields.length, + version: EcsVersion, + })} +

+ {PRE_BUILT_DETECTION_ENGINE_RULES_WORK} + {CUSTOM_DETECTION_ENGINE_RULES_WORK} + {PAGES_DISPLAY_EVENTS} + {OTHER_APP_CAPABILITIES_WORK_PROPERLY} + {ECS_COMPLIANT_MAPPINGS_ARE_FULLY_SUPPORTED} +
+ + + + ) : ( + + + + )} +
+ ); +}; + +EcsCompliantTabComponent.displayName = 'EcsCompliantTabComponent'; + +export const EcsCompliantTab = React.memo(EcsCompliantTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/translations.ts similarity index 51% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/translations.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/translations.ts index 79dfb1bcaac22..de9904af31a8d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/translations.ts @@ -7,16 +7,16 @@ import { i18n } from '@kbn/i18n'; -export const FAIL = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.fail', +export const ECS_VALUES = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsValues', { - defaultMessage: 'Fail', + defaultMessage: 'ECS values', } ); -export const PASS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.pass', +export const ECS_MAPPING_TYPE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsMappingType', { - defaultMessage: 'Pass', + defaultMessage: 'ECS mapping type', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.test.tsx new file mode 100644 index 0000000000000..ed727de33a14f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { omit } from 'lodash/fp'; + +import { getEcsCompliantTableColumns } from './get_ecs_compliant_table_columns'; +import { TestExternalProviders } from '../../../../../../../../mock/test_providers/test_providers'; +import { eventCategory } from '../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; + +describe('getEcsCompliantTableColumns', () => { + test('it returns the expected columns', () => { + expect(getEcsCompliantTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: 'ECS mapping type', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'allowed_values', + name: 'ECS values', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '35%', + }, + ]); + }); + + describe('type render()', () => { + describe('when `type` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const typeRender = columns[1].render; + + render( + + {typeRender != null && typeRender(eventCategory.type, eventCategory)} + + ); + }); + + test('it renders the expected `type`', () => { + expect(screen.getByTestId('type')).toHaveTextContent('keyword'); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('typePlaceholder')).not.toBeInTheDocument(); + }); + }); + }); + + describe('allowed values render()', () => { + describe('when `allowedValues` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const allowedValuesRender = columns[2].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender(eventCategory.allowed_values, eventCategory)} + + + ); + }); + + test('it renders the expected `AllowedValue` names', () => { + expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( + eventCategory.allowed_values?.map(({ name }) => name).join('') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); + }); + }); + + describe('when `allowedValues` is undefined', () => { + const withUndefinedAllowedValues = { + ...eventCategory, + allowed_values: undefined, // <-- + }; + + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const allowedValuesRender = columns[2].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender( + withUndefinedAllowedValues.allowed_values, + withUndefinedAllowedValues + )} + + + ); + }); + + test('it does NOT render the `AllowedValue` names', () => { + expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); + }); + }); + }); + + describe('description render()', () => { + describe('when `description` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const descriptionRender = columns[3].render; + + render( + + <> + {descriptionRender != null && + descriptionRender(eventCategory.description, eventCategory)} + + + ); + }); + + test('it renders the expected `description`', () => { + expect(screen.getByTestId('description')).toHaveTextContent( + eventCategory.description?.replaceAll('\n', ' ') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.tsx new file mode 100644 index 0000000000000..65e81477a1b63 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/ecs_compliant_tab/utils/get_ecs_compliant_table_columns.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTableFieldDataColumnType } from '@elastic/eui'; + +import { CodeSuccess } from '../../../../../../../../styles'; +import { AllowedValue, EcsCompliantFieldMetadata } from '../../../../../../../../types'; +import { FIELD } from '../../../../../../../../translations'; +import { EcsAllowedValues } from '../../../../ecs_allowed_values'; +import { ECS_MAPPING_TYPE, ECS_VALUES } from '../translations'; +import { ECS_DESCRIPTION } from '../../../../translations'; + +export const getEcsCompliantTableColumns = (): Array< + EuiTableFieldDataColumnType +> => [ + { + field: 'indexFieldName', + name: FIELD, + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: ECS_MAPPING_TYPE, + render: (type: string) => {type}, + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'allowed_values', + name: ECS_VALUES, + render: (allowedValues: AllowedValue[] | undefined) => ( + + ), + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: ECS_DESCRIPTION, + render: (description: string) => {description}, + sortable: false, + truncateText: false, + width: '35%', + }, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.test.tsx similarity index 92% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.test.tsx index 425c27d7afac5..1ee41f6b68487 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.test.tsx @@ -7,21 +7,21 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import { IndexCheckFields } from '.'; import { TestDataQualityProviders, TestExternalProviders, } from '../../../../../../mock/test_providers/test_providers'; import { auditbeatWithAllResults } from '../../../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; -import userEvent from '@testing-library/user-event'; +import { LatestCheckFields } from '.'; describe('IndexCheckFields', () => { beforeEach(() => { render( - { ); }); it('should render the index check fields', () => { - expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument(); + expect(screen.getByTestId('latestCheckFields')).toBeInTheDocument(); }); it('should render incompatible tab content by default', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.tsx new file mode 100644 index 0000000000000..918d85e3b5d60 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/index.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiButtonGroup } from '@elastic/eui'; +import styled from 'styled-components'; + +import { + ALL_FIELDS, + CUSTOM_FIELDS, + ECS_COMPLIANT_FIELDS, + INCOMPATIBLE_FIELDS, + SAME_FAMILY, +} from '../../../../../../translations'; +import { useIndicesCheckContext } from '../../../../../../contexts/indices_check_context'; +import { IlmPhase, PatternRollup } from '../../../../../../types'; +import { EMPTY_METADATA } from '../../../../../../constants'; +import { + ALL_TAB_ID, + CUSTOM_TAB_ID, + ECS_COMPLIANT_TAB_ID, + INCOMPATIBLE_TAB_ID, + SAME_FAMILY_TAB_ID, +} from '../../constants'; +import { getIncompatibleStatBadgeColor } from '../../../../../../utils/get_incompatible_stat_badge_color'; +import { getIncompatibleMappings, getIncompatibleValues } from '../../../../../../utils/markdown'; +import { IncompatibleTab } from '../../incompatible_tab'; +import { getSizeInBytes } from '../../../../../../utils/stats'; +import { SameFamilyTab } from '../../same_family_tab'; +import { CustomTab } from './custom_tab'; +import { EcsCompliantTab } from './ecs_compliant_tab'; +import { AllTab } from './all_tab'; +import { getEcsCompliantBadgeColor } from './utils/get_ecs_compliant_badge_color'; +import { CheckFieldsTabs } from '../../check_fields_tabs'; + +const StyledButtonGroup = styled(EuiButtonGroup)` + button[data-test-subj='${INCOMPATIBLE_TAB_ID}'] { + flex-grow: 1.2; + } + button[data-test-subj='${ECS_COMPLIANT_TAB_ID}'] { + flex-grow: 1.4; + } +`; + +export interface Props { + indexName: string; + patternRollup: PatternRollup | undefined; + ilmPhase: IlmPhase | undefined; + docsCount: number; +} + +const LatestCheckFieldsComponent: React.FC = ({ + indexName, + patternRollup, + ilmPhase, + docsCount, +}) => { + const { checkState } = useIndicesCheckContext(); + const partitionedFieldMetadata = + checkState[indexName]?.partitionedFieldMetadata ?? EMPTY_METADATA; + + const incompatibleFields = partitionedFieldMetadata.incompatible; + const incompatibleFieldsCount = incompatibleFields.length; + const incompatibleMappingsFields = getIncompatibleMappings(incompatibleFields); + const incompatibleValuesFields = getIncompatibleValues(incompatibleFields); + const ecsCompliantFields = partitionedFieldMetadata.ecsCompliant; + const ecsCompliantFieldsCount = ecsCompliantFields.length; + const customFields = partitionedFieldMetadata.custom; + const customFieldsCount = customFields.length; + const sameFamilyFields = partitionedFieldMetadata.sameFamily; + const sameFamilyFieldsCount = sameFamilyFields.length; + const allFields = partitionedFieldMetadata.all; + const allFieldsCount = allFields.length; + const sizeInBytes = useMemo( + () => + getSizeInBytes({ + indexName, + stats: patternRollup?.stats ?? null, + }), + [indexName, patternRollup?.stats] + ); + const patternDocsCount = patternRollup?.docsCount ?? 0; + + const tabs = useMemo( + () => [ + { + id: INCOMPATIBLE_TAB_ID, + name: INCOMPATIBLE_FIELDS, + badgeColor: getIncompatibleStatBadgeColor(incompatibleFieldsCount), + badgeCount: incompatibleFieldsCount, + content: ( + + ), + }, + { + id: SAME_FAMILY_TAB_ID, + name: SAME_FAMILY, + badgeCount: sameFamilyFieldsCount, + content: ( + + ), + }, + { + id: CUSTOM_TAB_ID, + name: CUSTOM_FIELDS, + badgeCount: customFieldsCount, + content: ( + + ), + }, + + { + id: ECS_COMPLIANT_TAB_ID, + name: ECS_COMPLIANT_FIELDS, + badgeColor: getEcsCompliantBadgeColor(ecsCompliantFields), + badgeCount: ecsCompliantFieldsCount, + content: , + }, + { + id: ALL_TAB_ID, + name: ALL_FIELDS, + badgeCount: allFieldsCount, + content: , + }, + ], + [ + allFields, + allFieldsCount, + customFields, + customFieldsCount, + docsCount, + ecsCompliantFields, + ecsCompliantFieldsCount, + ilmPhase, + incompatibleFieldsCount, + incompatibleMappingsFields, + incompatibleValuesFields, + indexName, + patternDocsCount, + sameFamilyFields, + sameFamilyFieldsCount, + sizeInBytes, + ] + ); + + return ( +
+ } + /> +
+ ); +}; + +LatestCheckFieldsComponent.displayName = 'LatestCheckFieldsComponent'; + +export const LatestCheckFields = React.memo(LatestCheckFieldsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/sticky_actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/sticky_actions/index.tsx similarity index 96% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/sticky_actions/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/sticky_actions/index.tsx index 1cd2630e720d1..64d7ab337f35d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/sticky_actions/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/sticky_actions/index.tsx @@ -7,9 +7,9 @@ import React, { FC } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; - import styled from 'styled-components'; -import { Actions } from '../../../../../../../../actions'; + +import { Actions } from '../../../../../../../actions'; export const CopyToClipboardButton = styled(EuiButtonEmpty)` margin-left: ${({ theme }) => theme.eui.euiSizeXS}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.test.ts new file mode 100644 index 0000000000000..82ebeac4af599 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockPartitionedFieldMetadata } from '../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { getEcsCompliantBadgeColor } from './get_ecs_compliant_badge_color'; + +describe('getEcsCompliantBadgeColor', () => { + test('it returns the expected color for the ECS compliant data when the data includes an @timestamp', () => { + expect(getEcsCompliantBadgeColor(mockPartitionedFieldMetadata.ecsCompliant)).toBe('hollow'); + }); + + test('it returns the expected color for the ECS compliant data does NOT includes an @timestamp', () => { + const noTimestamp = mockPartitionedFieldMetadata.ecsCompliant.filter( + ({ name }) => name !== '@timestamp' + ); + + expect(getEcsCompliantBadgeColor(noTimestamp)).toEqual('danger'); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.ts new file mode 100644 index 0000000000000..9dce806e5b434 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/get_ecs_compliant_badge_color.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsCompliantFieldMetadata } from '../../../../../../../types'; +import { isTimestampFieldMissing } from './is_timestamp_field_missing'; + +/** + * Determines the badge color for ECS compliant fields + */ +export const getEcsCompliantBadgeColor = ( + ecsCompliantFields: EcsCompliantFieldMetadata[] +): string => (isTimestampFieldMissing(ecsCompliantFields) ? 'danger' : 'hollow'); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.test.ts new file mode 100644 index 0000000000000..1e72a2077e5e4 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + eventCategory, + timestamp, +} from '../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { isTimestampFieldMissing } from './is_timestamp_field_missing'; + +describe('isTimestampFieldMissing', () => { + test('it returns true when `enrichedFieldMetadata` is empty', () => { + expect(isTimestampFieldMissing([])).toBe(true); + }); + + test('it returns false when `enrichedFieldMetadata` contains an @timestamp field', () => { + expect(isTimestampFieldMissing([timestamp, eventCategory])).toBe(false); + }); + + test('it returns true when `enrichedFieldMetadata` does NOT contain an @timestamp field', () => { + expect(isTimestampFieldMissing([eventCategory])).toBe(true); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.ts new file mode 100644 index 0000000000000..8c208c3b00919 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/latest_results/latest_check_fields/utils/is_timestamp_field_missing.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsBasedFieldMetadata } from '../../../../../../../types'; + +/** + * Checks if the timestamp field is missing + */ +export const isTimestampFieldMissing = (ecsBasedFieldMetadata: EcsBasedFieldMetadata[]): boolean => + !ecsBasedFieldMetadata.some((x) => x.name === '@timestamp'); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.test.tsx similarity index 80% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.test.tsx index 210fcc3a2c43b..3bf3f68d536e7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.test.tsx @@ -8,9 +8,9 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; import { SameFamily } from '.'; -import { SAME_FAMILY_BADGE_LABEL } from '../../../translate'; +import { SAME_FAMILY_BADGE_LABEL } from '../../../../../translations'; describe('SameFamily', () => { test('it renders a badge with the expected content', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.tsx similarity index 91% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.tsx index 3ca603944c0d5..afef2c045d05f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/compare_fields_table/same_family/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family/index.tsx @@ -8,7 +8,8 @@ import { EuiBadge } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { SAME_FAMILY_BADGE_LABEL } from '../../../translate'; + +import { SAME_FAMILY_BADGE_LABEL } from '../../../../../translations'; const SameFamilyBadge = styled(EuiBadge)` margin: ${({ theme }) => `0 ${theme.eui.euiSizeXS}`}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.test.tsx similarity index 70% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.test.tsx index 39289f1a9294f..445ba3ba8776d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.test.tsx @@ -9,18 +9,15 @@ import { EcsVersion } from '@elastic/ecs'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { FIELDS_WITH_MAPPINGS_SAME_FAMILY } from '../../../../translations'; -import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; +import { FIELDS_WITH_MAPPINGS_SAME_FAMILY } from '../translations'; import { SameFamilyCallout } from '.'; -import { mockPartitionedFieldMetadataWithSameFamily } from '../../../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; describe('SameFamilyCallout', () => { beforeEach(() => { render( - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.tsx similarity index 68% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.tsx index ba7bab71f228d..04861e4e25ace 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/callouts/same_family_callout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_callout/index.tsx @@ -8,27 +8,25 @@ import { EcsVersion } from '@elastic/ecs'; import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; - -import * as i18n from '../../../../translations'; -import type { EcsBasedFieldMetadata } from '../../../../../../../../../types'; +import { FIELDS_WITH_MAPPINGS_SAME_FAMILY, SAME_FAMILY_CALLOUT } from '../translations'; interface Props { - ecsBasedFieldMetadata: EcsBasedFieldMetadata[]; + fieldCount: number; } -const SameFamilyCalloutComponent: React.FC = ({ ecsBasedFieldMetadata }) => { +const SameFamilyCalloutComponent: React.FC = ({ fieldCount }) => { return (
- {i18n.SAME_FAMILY_CALLOUT({ - fieldCount: ecsBasedFieldMetadata.length, + {SAME_FAMILY_CALLOUT({ + fieldCount, version: EcsVersion, })}
- {i18n.FIELDS_WITH_MAPPINGS_SAME_FAMILY} + {FIELDS_WITH_MAPPINGS_SAME_FAMILY}
diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/index.tsx new file mode 100644 index 0000000000000..575a8bd82c1f6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/index.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { Actions } from '../../../../../actions'; +import { SameFamilyCallout } from '../same_family_callout'; +import { CompareFieldsTable } from '../compare_fields_table'; +import { useDataQualityContext } from '../../../../../data_quality_context'; +import { getAllSameFamilyMarkdownComments } from './utils/markdown'; +import type { IlmPhase, SameFamilyFieldMetadata } from '../../../../../types'; +import { StickyActions } from '../latest_results/latest_check_fields/sticky_actions'; +import { getSameFamilyTableColumns } from './utils/get_same_family_table_columns'; +import { SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE } from '../translations'; + +interface Props { + docsCount: number; + ilmPhase: IlmPhase | undefined; + indexName: string; + patternDocsCount?: number; + sizeInBytes: number | undefined; + sameFamilyFields: SameFamilyFieldMetadata[]; + incompatibleFieldsCount: number; + customFieldsCount: number; + ecsCompliantFieldsCount: number; + allFieldsCount: number; + hasStickyActions?: boolean; +} + +const SameFamilyTabComponent: React.FC = ({ + docsCount, + ilmPhase, + indexName, + patternDocsCount, + sizeInBytes, + sameFamilyFields, + incompatibleFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + hasStickyActions = true, +}) => { + const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); + const markdownComment: string = useMemo( + () => + getAllSameFamilyMarkdownComments({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + sameFamilyFields, + incompatibleFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, + }).join('\n'), + [ + allFieldsCount, + customFieldsCount, + docsCount, + ecsCompliantFieldsCount, + formatBytes, + formatNumber, + ilmPhase, + incompatibleFieldsCount, + indexName, + isILMAvailable, + patternDocsCount, + sameFamilyFields, + sizeInBytes, + ] + ); + + return ( +
+ + + <> + {sameFamilyFields.length > 0 && ( + <> + + + + + )} + + + 0 ? 'm' : 'l'} /> + {hasStickyActions ? ( + + ) : ( + + )} +
+ ); +}; + +SameFamilyTabComponent.displayName = 'SameFamilyTabComponent'; + +export const SameFamilyTab = React.memo(SameFamilyTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.test.tsx new file mode 100644 index 0000000000000..b0aae5885f096 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { omit } from 'lodash/fp'; +import { render, screen } from '@testing-library/react'; + +import { getSameFamilyTableColumns } from './get_same_family_table_columns'; +import { TestExternalProviders } from '../../../../../../mock/test_providers/test_providers'; +import { mockAgentTypeSameFamilyField } from '../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { SAME_FAMILY_BADGE_LABEL } from '../../../../../../translations'; + +describe('getSameFamilyTableColumns', () => { + test('it returns the expected column configuration', () => { + const columns = getSameFamilyTableColumns().map((x) => omit('render', x)); + + expect(columns).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: 'ECS mapping type (expected)', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'indexFieldType', + name: 'Index mapping type (actual)', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '35%', + }, + ]); + }); + + describe('type column render()', () => { + test('it renders the expected type', () => { + const columns = getSameFamilyTableColumns(); + const typeColumnRender = columns[1].render; + const expected = 'keyword'; + + render( + + {typeColumnRender != null && + typeColumnRender(mockAgentTypeSameFamilyField.type, mockAgentTypeSameFamilyField)} + + ); + + expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected); + }); + }); + + describe('indexFieldType column render()', () => { + beforeEach(() => { + const columns = getSameFamilyTableColumns(); + const indexFieldTypeColumnRender = columns[2].render; + + render( + + {indexFieldTypeColumnRender != null && + indexFieldTypeColumnRender( + mockAgentTypeSameFamilyField.indexFieldType, + mockAgentTypeSameFamilyField + )} + + ); + }); + + test('it renders the expected type with a "success" style', () => { + expect(screen.getByTestId('codeSuccess')).toHaveTextContent( + mockAgentTypeSameFamilyField.indexFieldType + ); + }); + + test('it renders the same family badge', () => { + expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY_BADGE_LABEL); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.tsx new file mode 100644 index 0000000000000..713f55c604034 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/get_same_family_table_columns.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTableFieldDataColumnType } from '@elastic/eui'; + +import { SameFamilyFieldMetadata } from '../../../../../../types'; +import { + ECS_MAPPING_TYPE_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, +} from '../../../../../../translations'; +import { CodeSuccess } from '../../../../../../styles'; +import { SameFamily } from '../../same_family'; +import { ECS_DESCRIPTION } from '../../translations'; + +export const getSameFamilyTableColumns = (): Array< + EuiTableFieldDataColumnType +> => [ + { + field: 'indexFieldName', + name: FIELD, + sortable: true, + truncateText: false, + width: '15%', + }, + { + field: 'type', + name: ECS_MAPPING_TYPE_EXPECTED, + render: (type: string) => {type}, + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'indexFieldType', + name: INDEX_MAPPING_TYPE_ACTUAL, + render: (indexFieldType: string) => ( +
+ {indexFieldType} + +
+ ), + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: ECS_DESCRIPTION, + sortable: false, + truncateText: false, + width: '35%', + }, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.test.ts similarity index 82% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.test.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.test.ts index 6eedd81fae4a5..c8b98b1611f61 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/same_family_tab/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.test.ts @@ -11,11 +11,12 @@ import { EcsVersion } from '@elastic/ecs'; import { getAllSameFamilyMarkdownComments, getSameFamilyMarkdownComment, + getSameFamilyMarkdownTableRows, getSameFamilyMarkdownTablesComment, -} from './helpers'; -import { EMPTY_STAT } from '../../../../../../../../constants'; -import { mockPartitionedFieldMetadata } from '../../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { mockPartitionedFieldMetadataWithSameFamily } from '../../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; +} from './markdown'; +import { EMPTY_STAT } from '../../../../../../constants'; +import { mockPartitionedFieldMetadataWithSameFamily } from '../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; +import { mockSameFamilyFields } from '../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; describe('helpers', () => { describe('getSameFamilyMarkdownComment', () => { @@ -35,7 +36,7 @@ Fields with mappings in the same family have exactly the same search behavior as expect( getSameFamilyMarkdownTablesComment({ - sameFamilyMappings: [ + sameFamilyFields: [ { dashed_name: 'agent-type', description: @@ -64,7 +65,7 @@ Fields with mappings in the same family have exactly the same search behavior as test('it returns the expected comment when the index does NOT have same family mappings', () => { expect( getSameFamilyMarkdownTablesComment({ - sameFamilyMappings: [], + sameFamilyFields: [], indexName: 'auditbeat-custom-index-1', }) ).toEqual('\n\n'); @@ -89,7 +90,11 @@ Fields with mappings in the same family have exactly the same search behavior as ilmPhase: 'unmanaged', indexName: 'auditbeat-custom-index-1', isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadataWithSameFamily, + incompatibleFieldsCount: 3, + sameFamilyFields: mockPartitionedFieldMetadataWithSameFamily.sameFamily, + ecsCompliantFieldsCount: 2, + customFieldsCount: 4, + allFieldsCount: 10, patternDocsCount: 57410, sizeInBytes: 28413, }) @@ -111,7 +116,11 @@ Fields with mappings in the same family have exactly the same search behavior as ilmPhase: 'unmanaged', indexName: 'auditbeat-custom-index-1', isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, + incompatibleFieldsCount: 3, + sameFamilyFields: [], + ecsCompliantFieldsCount: 2, + customFieldsCount: 4, + allFieldsCount: 9, patternDocsCount: 57410, sizeInBytes: 28413, }) @@ -123,4 +132,12 @@ Fields with mappings in the same family have exactly the same search behavior as ]); }); }); + + describe('getSameFamilyMarkdownTableRows', () => { + test('it returns the expected table rows when the field is in the same family', () => { + expect(getSameFamilyMarkdownTableRows(mockSameFamilyFields)).toEqual( + '| agent.type | `keyword` | `constant_keyword` `same family` |\n| host.name | `keyword` | `constant_keyword` `same family` |' + ); + }); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.ts new file mode 100644 index 0000000000000..7a2c89d42f3d6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/same_family_tab/utils/markdown.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsVersion } from '@elastic/ecs'; + +import { + ECS_MAPPING_TYPE_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, + SAME_FAMILY_BADGE_LABEL, +} from '../../../../../../translations'; +import { + escapeNewlines, + getCodeFormattedValue, + getMarkdownComment, + getMarkdownTable, + getSummaryMarkdownComment, + getSummaryTableMarkdownComment, + getTabCountsMarkdownComment, +} from '../../../../../../utils/markdown'; +import type { IlmPhase, SameFamilyFieldMetadata } from '../../../../../../types'; +import { + FIELDS_WITH_MAPPINGS_SAME_FAMILY, + SAME_FAMILY_CALLOUT, + SAME_FAMILY_CALLOUT_TITLE, + SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE, +} from '../../translations'; + +export const getSameFamilyMarkdownComment = (fieldsInSameFamily: number): string => + getMarkdownComment({ + suggestedAction: `${SAME_FAMILY_CALLOUT({ + fieldCount: fieldsInSameFamily, + version: EcsVersion, + })} + +${FIELDS_WITH_MAPPINGS_SAME_FAMILY} +`, + title: SAME_FAMILY_CALLOUT_TITLE(fieldsInSameFamily), + }); + +export const getSameFamilyMarkdownTableRows = ( + sameFamilyFields: SameFamilyFieldMetadata[] +): string => + sameFamilyFields + .map( + (x) => + `| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue( + x.type + )} | ${getCodeFormattedValue(x.indexFieldType)} ${getCodeFormattedValue( + SAME_FAMILY_BADGE_LABEL + )} |` + ) + .join('\n'); + +export const getSameFamilyMarkdownTablesComment = ({ + sameFamilyFields, + indexName, +}: { + sameFamilyFields: SameFamilyFieldMetadata[]; + indexName: string; +}): string => ` +${ + sameFamilyFields.length > 0 + ? getMarkdownTable({ + enrichedFieldMetadata: sameFamilyFields, + getMarkdownTableRows: getSameFamilyMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + : '' +} +`; + +export const getAllSameFamilyMarkdownComments = ({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + sameFamilyFields, + incompatibleFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, +}: { + docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + ilmPhase: IlmPhase | undefined; + indexName: string; + isILMAvailable: boolean; + sameFamilyFields: SameFamilyFieldMetadata[]; + incompatibleFieldsCount: number; + customFieldsCount: number; + ecsCompliantFieldsCount: number; + allFieldsCount: number; + patternDocsCount?: number; + sizeInBytes: number | undefined; +}): string[] => { + const sameFamilyMarkdownComment = + sameFamilyFields.length > 0 ? getSameFamilyMarkdownComment(sameFamilyFields.length) : ''; + + return [ + getSummaryMarkdownComment(indexName), + getSummaryTableMarkdownComment({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleFieldsCount, + patternDocsCount, + sizeInBytes, + }), + getTabCountsMarkdownComment({ + incompatibleFieldsCount, + sameFamilyFieldsCount: sameFamilyFields.length, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + }), + sameFamilyMarkdownComment, + getSameFamilyMarkdownTablesComment({ + sameFamilyFields, + indexName, + }), + ].filter((x) => x !== ''); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/styles.tsx similarity index 58% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/styles.tsx index 2a7172d58a7b4..f899d42026178 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/styles.tsx @@ -5,10 +5,8 @@ * 2.0. */ -export const getIndexPropertiesContainerId = ({ - indexName, - pattern, -}: { - indexName: string; - pattern: string; -}): string => `index-properties-container-${pattern}${indexName}`; +import styled from 'styled-components'; + +export const CalloutItem = styled.div` + margin-left: ${({ theme }) => theme.eui.euiSizeS}; +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/translations.ts index e5915404cba02..45f9873a764ac 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/translations.ts @@ -7,9 +7,410 @@ import { i18n } from '@kbn/i18n'; -export const CHECK_NOW: string = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.checkNowButton', +export const HISTORY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.historyTab', { - defaultMessage: 'Check now', + defaultMessage: 'History', } ); + +export const LATEST_CHECK = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.latestCheckTab', + { + defaultMessage: 'Latest Check', + } +); + +export const ADD_TO_NEW_CASE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton', + { + defaultMessage: 'Add to new case', + } +); + +export const ALL_CALLOUT = (version: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCallout', { + values: { version }, + defaultMessage: + "All mappings for the fields in this index, including fields that comply with the Elastic Common Schema (ECS), version {version}, and fields that don't", + }); + +export const ALL_CALLOUT_TITLE = (fieldCount: number) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutTitle', + { + values: { fieldCount }, + defaultMessage: + 'All {fieldCount} {fieldCount, plural, =1 {field mapping} other {field mappings}}', + } + ); + +export const ALL_EMPTY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyContent', + { + defaultMessage: 'This index does not contain any mappings', + } +); + +export const ALL_EMPTY_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyTitle', + { + defaultMessage: 'No mappings', + } +); + +export const ALL_FIELDS_TABLE_TITLE = (indexName: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.allTab.allFieldsTableTitle', { + values: { indexName }, + defaultMessage: 'All fields - {indexName}', + }); + +export const SUMMARY_MARKDOWN_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle', + { + defaultMessage: 'Data quality', + } +); + +export const SUMMARY_MARKDOWN_DESCRIPTION = ({ + ecsFieldReferenceUrl, + ecsReferenceUrl, + indexName, + mappingUrl, + version, +}: { + ecsFieldReferenceUrl: string; + ecsReferenceUrl: string; + indexName: string; + mappingUrl: string; + version: string; +}) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription', + { + values: { ecsFieldReferenceUrl, ecsReferenceUrl, indexName, mappingUrl, version }, + defaultMessage: + 'The `{indexName}` index has [mappings]({mappingUrl}) or field values that are different than the [Elastic Common Schema]({ecsReferenceUrl}) (ECS), version `{version}` [definitions]({ecsFieldReferenceUrl}).', + } + ); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.copyToClipboardButton', + { + defaultMessage: 'Copy to clipboard', + } +); + +export const CUSTOM_FIELDS_TABLE_TITLE = (indexName: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.customTab.customFieldsTableTitle', + { + values: { indexName }, + defaultMessage: 'Custom fields - {indexName}', + } + ); + +export const CUSTOM_DETECTION_ENGINE_RULES_WORK = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage', + { + defaultMessage: '✅ Custom detection engine rules work', + } +); + +export const ECS_COMPLIANT_CALLOUT = ({ + fieldCount, + version, +}: { + fieldCount: number; + version: string; +}) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout', + { + values: { fieldCount, version }, + defaultMessage: + 'The {fieldCount, plural, =1 {index mapping type and document values for this field comply} other {index mapping types and document values of these fields comply}} with the Elastic Common Schema (ECS), version {version}', + } + ); + +export const ECS_COMPLIANT_CALLOUT_TITLE = (fieldCount: number) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle', + { + values: { fieldCount }, + defaultMessage: '{fieldCount} ECS compliant {fieldCount, plural, =1 {field} other {fields}}', + } + ); + +export const ECS_COMPLIANT_EMPTY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent', + { + defaultMessage: + 'None of the field mappings in this index comply with the Elastic Common Schema (ECS). The index must (at least) contain an @timestamp date field.', + } +); + +export const ECS_VERSION_MARKDOWN_COMMENT = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsVersionMarkdownComment', + { + defaultMessage: 'Elastic Common Schema (ECS) version', + } +); + +export const INDEX = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown', + { + defaultMessage: 'Index', + } +); + +export const ECS_COMPLIANT_EMPTY_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyTitle', + { + defaultMessage: 'No ECS compliant Mappings', + } +); + +export const ECS_COMPLIANT_MAPPINGS_ARE_FULLY_SUPPORTED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantMappingsAreFullySupportedMessage', + { + defaultMessage: '✅ ECS compliant mappings and field values are fully supported', + } +); + +export const ERROR_LOADING_MAPPINGS_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle', + { + defaultMessage: 'Unable to load index mappings', + } +); + +export const ERROR_LOADING_MAPPINGS_BODY = (error: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody', + { + values: { error }, + defaultMessage: 'There was a problem loading mappings: {error}', + } + ); + +export const ERROR_LOADING_UNALLOWED_VALUES_BODY = (error: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesBody', + { + values: { error }, + defaultMessage: 'There was a problem loading unallowed values: {error}', + } + ); + +export const ERROR_LOADING_UNALLOWED_VALUES_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesTitle', + { + defaultMessage: 'Unable to load unallowed values', + } +); + +export const ERROR_GENERIC_CHECK_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorGenericCheckTitle', + { + defaultMessage: 'An error occurred during the check', + } +); + +export const ECS_COMPLIANT_FIELDS_TABLE_TITLE = (indexName: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.customTab.ecsComplaintFieldsTableTitle', + { + values: { indexName }, + defaultMessage: 'ECS complaint fields - {indexName}', + } + ); + +export const LOADING_MAPPINGS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingMappingsPrompt', + { + defaultMessage: 'Loading mappings', + } +); + +export const LOADING_UNALLOWED_VALUES = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingUnallowedValuesPrompt', + { + defaultMessage: 'Loading unallowed values', + } +); + +export const CHECKING_INDEX = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.checkingIndexPrompt', + { + defaultMessage: 'Checking index', + } +); + +export const CUSTOM_CALLOUT = ({ fieldCount, version }: { fieldCount: number; version: string }) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCallout', { + values: { fieldCount, version }, + defaultMessage: + '{fieldCount, plural, =1 {This field is not} other {These fields are not}} defined by the Elastic Common Schema (ECS), version {version}.', + }); + +export const SAME_FAMILY_CALLOUT = ({ + fieldCount, + version, +}: { + fieldCount: number; + version: string; +}) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCallout', + { + values: { fieldCount, version }, + defaultMessage: + "{fieldCount, plural, =1 {This field is} other {These fields are}} defined by the Elastic Common Schema (ECS), version {version}, but {fieldCount, plural, =1 {its mapping type doesn't} other {their mapping types don't}} exactly match.", + } + ); + +export const CUSTOM_CALLOUT_TITLE = (fieldCount: number) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCalloutTitle', + { + values: { fieldCount }, + defaultMessage: + '{fieldCount} Custom {fieldCount, plural, =1 {field mapping} other {field mappings}}', + } + ); + +export const SAME_FAMILY_CALLOUT_TITLE = (fieldCount: number) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCalloutTitle', + { + values: { fieldCount }, + defaultMessage: + '{fieldCount} Same family {fieldCount, plural, =1 {field mapping} other {field mappings}}', + } + ); + +export const CUSTOM_EMPTY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyContent', + { + defaultMessage: 'All the field mappings in this index are defined by the Elastic Common Schema', + } +); + +export const CUSTOM_EMPTY_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyTitle', + { + defaultMessage: 'All field mappings defined by ECS', + } +); + +export const FIELDS_WITH_MAPPINGS_SAME_FAMILY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.fieldsWithMappingsSameFamilyLabel', + { + defaultMessage: + 'Fields with mappings in the same family have exactly the same search behavior as the type specified by ECS, but may have different space usage or performance characteristics.', + } +); + +export const WHEN_A_FIELD_IS_INCOMPATIBLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.whenAFieldIsIncompatibleLabel', + { + defaultMessage: 'When a field is incompatible:', + } +); + +export const DETECTION_ENGINE_RULES_WILL_WORK = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage', + { + defaultMessage: '✅ Detection engine rules will work for these fields', + } +); + +export const OTHER_APP_CAPABILITIES_WORK_PROPERLY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.otherAppCapabilitiesWorkProperlyMessage', + { + defaultMessage: '✅ Other app capabilities work properly', + } +); + +export const SAME_FAMILY_EMPTY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyContent', + { + defaultMessage: + 'All of the field mappings and document values in this index are compliant with the Elastic Common Schema (ECS).', + } +); + +export const SAME_FAMILY_EMPTY_TITLE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyTitle', + { + defaultMessage: 'All field mappings and values are ECS compliant', + } +); + +export const PAGES_DISPLAY_EVENTS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesDisplayEventsMessage', + { + defaultMessage: '✅ Pages display events and fields correctly', + } +); + +export const PAGES_MAY_NOT_DISPLAY_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayFieldsMessage', + { + defaultMessage: '🌕 Some pages and features may not display these fields', + } +); + +export const PRE_BUILT_DETECTION_ENGINE_RULES_WORK = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.preBuiltDetectionEngineRulesWorkMessage', + { + defaultMessage: '✅ Pre-built detection engine rules work', + } +); + +export const ECS_IS_A_PERMISSIVE_SCHEMA = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsIsAPermissiveSchemaMessage', + { + defaultMessage: + 'ECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.', + } +); + +export const SOMETIMES_INDICES_CREATED_BY_OLDER = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription', + { + defaultMessage: + 'Sometimes, indices created by older integrations will have mappings or values that were, but are no longer compliant.', + } +); + +export const UNKNOWN = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel', + { + defaultMessage: 'Unknown', + } +); + +export const ECS_DESCRIPTION = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsDescription', + { + defaultMessage: 'ECS description', + } +); + +export const SEARCH_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder', + { + defaultMessage: 'Search fields', + } +); + +export const SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE = (indexName: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle', + { + values: { indexName }, + defaultMessage: 'Same family field mappings - {indexName}', + } + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/types.ts new file mode 100644 index 0000000000000..a5b49351dcee1 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpHandler } from '@kbn/core-http-browser'; +import { HISTORY_TAB_ID, LATEST_CHECK_TAB_ID } from '../constants'; + +export interface FetchHistoricalResultsOpts extends Partial { + indexName: string; + httpFetch: HttpHandler; + abortController: AbortController; +} + +export type UseHistoricalResultsFetchOpts = Omit; + +export type UseHistoricalResultsFetch = (opts: UseHistoricalResultsFetchOpts) => Promise; + +export interface FetchHistoricalResultsQueryState { + from: number; + size: number; + startDate: string; + endDate: string; + outcome?: 'pass' | 'fail'; +} + +export type IndexCheckFlyoutTabId = typeof HISTORY_TAB_ID | typeof LATEST_CHECK_TAB_ID; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts new file mode 100644 index 0000000000000..9956bbc525aa8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment-timezone'; + +moment.tz.setDefault('UTC'); + +import { getFormattedCheckTime } from './get_formatted_check_time'; + +describe('getFormattedCheckTime', () => { + it('returns formatted check time', () => { + const formattedCheckTime = getFormattedCheckTime(1613474400000); + expect(formattedCheckTime).toBe('Feb 16, 2021 @ 11:20:00'); + }); + + describe('when check time is invalid', () => { + it('returns -- string', () => { + const formattedCheckTime = getFormattedCheckTime(Infinity); + expect(formattedCheckTime).toBe('--'); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts new file mode 100644 index 0000000000000..c5b99e4d12826 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; + +import { EMPTY_STAT } from '../../../../../constants'; + +export const getFormattedCheckTime = (checkedAt: number) => + moment(checkedAt).isValid() ? moment(checkedAt).format('MMM DD, YYYY @ HH:mm:ss') : EMPTY_STAT; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx index 5128130971b09..979445bca8af7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx @@ -10,8 +10,8 @@ import React from 'react'; import styled from 'styled-components'; import { getIndexResultToolTip } from '../utils/get_index_result_tooltip'; -import { getIndexResultBadgeColor } from './utils/get_index_result_badge_color'; -import * as i18n from './translations'; +import { getCheckTextColor } from '../utils/get_check_text_color'; +import { FAIL, PASS } from '../translations'; const StyledBadge = styled(EuiBadge)` width: 44px; @@ -37,10 +37,10 @@ export const IndexResultBadgeComponent: React.FC = ({ - {incompatible > 0 ? i18n.FAIL : i18n.PASS} + {incompatible > 0 ? FAIL : PASS} ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx index f88c9b8f977b0..7d4d7a7a3e1c0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx @@ -44,7 +44,7 @@ const PatternLabelComponent: React.FC = ({ return ( - {showResult(resultOpts) && ( + {indices != null && indices > 0 && showResult(resultOpts) && ( [ - indexName, - { - isChecking: false, - isLoadingMappings: false, - isLoadingUnallowedValues: false, - indexes: null, - mappingsProperties: null, - unallowedValues: null, - genericError: null, - mappingsError: null, - unallowedValuesError: null, - partitionedFieldMetadata: null, - isCheckComplete: false, - searchResults: null, - }, - ]) - ), + onViewHistoryAction: jest.fn(), }; describe('SummaryTable', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx index bc4e572e892fa..fa574362e7d9b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx @@ -12,7 +12,6 @@ import React, { useCallback, useMemo } from 'react'; import { defaultSort } from '../../../../constants'; import { IndexSummaryTableItem, SortConfig } from '../../../../types'; import { useDataQualityContext } from '../../../../data_quality_context'; -import { UseIndicesCheckCheckState } from '../../../../hooks/use_indices_check/types'; import { MIN_PAGE_SIZE } from '../constants'; import { getShowPagination } from './utils/get_show_pagination'; @@ -20,19 +19,17 @@ export interface Props { getTableColumns: ({ formatBytes, formatNumber, - checkState, isILMAvailable, pattern, - onExpandAction, onCheckNowAction, + onViewHistoryAction, }: { formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; - checkState: UseIndicesCheckCheckState; isILMAvailable: boolean; pattern: string; - onExpandAction: (indexName: string) => void; onCheckNowAction: (indexName: string) => void; + onViewHistoryAction: (indexName: string) => void; }) => Array>; items: IndexSummaryTableItem[]; pageIndex: number; @@ -42,9 +39,8 @@ export interface Props { setPageSize: (pageSize: number) => void; setSorting: (sortConfig: SortConfig) => void; sorting: SortConfig; - onExpandAction: (indexName: string) => void; onCheckNowAction: (indexName: string) => void; - checkState: UseIndicesCheckCheckState; + onViewHistoryAction: (indexName: string) => void; } const SummaryTableComponent: React.FC = ({ @@ -57,9 +53,8 @@ const SummaryTableComponent: React.FC = ({ setPageSize, setSorting, sorting, - onExpandAction, onCheckNowAction, - checkState, + onViewHistoryAction, }) => { const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); const columns = useMemo( @@ -67,21 +62,19 @@ const SummaryTableComponent: React.FC = ({ getTableColumns({ formatBytes, formatNumber, - checkState, isILMAvailable, pattern, - onExpandAction, onCheckNowAction, + onViewHistoryAction, }), [ + getTableColumns, formatBytes, formatNumber, - getTableColumns, - checkState, isILMAvailable, - onCheckNowAction, - onExpandAction, pattern, + onCheckNowAction, + onViewHistoryAction, ] ); const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts index c3302d7be4b57..f60a3ea86107b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts @@ -14,13 +14,6 @@ export const COLLAPSE = i18n.translate( } ); -export const VIEW_CHECK_DETAILS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.viewCheckDetailsLabel', - { - defaultMessage: 'View check details', - } -); - export const EXPAND_ROWS = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn', { @@ -55,9 +48,9 @@ export const ACTIONS = i18n.translate( } ); -export const CHECK_INDEX = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.checkIndexButton', +export const VIEW_HISTORY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.viewHistory', { - defaultMessage: 'Check index', + defaultMessage: 'View history', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx index 329a8eab4bec4..eda93c45f3b4f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx @@ -24,9 +24,9 @@ import { getSummaryTableILMPhaseColumn, getSummaryTableSizeInBytesColumn, } from './columns'; -import { CHECK_INDEX, VIEW_CHECK_DETAILS } from '../translations'; +import { VIEW_HISTORY } from '../translations'; import { IndexSummaryTableItem } from '../../../../../types'; -import { getCheckState } from '../../../../../stub/get_check_state'; +import { CHECK_NOW } from '../../translations'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -64,8 +64,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }).map((x) => omit('render', x)); expect(columns).toEqual([ @@ -75,11 +74,11 @@ describe('helpers', () => { width: '65px', actions: [ { - name: 'View check details', + name: 'Check now', render: expect.any(Function), }, { - name: 'Check index', + name: 'View history', render: expect.any(Function), }, ], @@ -119,19 +118,18 @@ describe('helpers', () => { }); describe('action columns render()', () => { - test('it renders check index button', () => { + test('it renders check now button', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const checkNowRender = ( (columns[0] as EuiTableActionsColumnType) - .actions[1] as CustomItemAction + .actions[0] as CustomItemAction ).render; render( @@ -140,10 +138,10 @@ describe('helpers', () => {
); - expect(screen.getByLabelText(CHECK_INDEX)).toBeInTheDocument(); + expect(screen.getByLabelText(CHECK_NOW)).toBeInTheDocument(); }); - test('it invokes the `onCheckNowAction` with the index name when the check index button is clicked', async () => { + test('it invokes the `onCheckNowAction` with the index name when the check now button is clicked', async () => { const onCheckNowAction = jest.fn(); const columns = getSummaryTableColumns({ @@ -152,12 +150,11 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction, - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const checkNowRender = ( (columns[0] as EuiTableActionsColumnType) - .actions[1] as CustomItemAction + .actions[0] as CustomItemAction ).render; render( @@ -166,40 +163,14 @@ describe('helpers', () => { ); - const button = screen.getByLabelText(CHECK_INDEX); + const button = screen.getByLabelText(CHECK_NOW); await userEvent.click(button); expect(onCheckNowAction).toBeCalledWith(indexSummaryTableItem.indexName); }); - test('it renders disabled check index with loading indicator when check state is loading', () => { - const columns = getSummaryTableColumns({ - formatBytes, - formatNumber, - isILMAvailable, - pattern: 'auditbeat-*', - onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName, { isChecking: true }), - }); - - const checkNowRender = ( - (columns[0] as EuiTableActionsColumnType) - .actions[1] as CustomItemAction - ).render; - - render( - - {checkNowRender != null && checkNowRender(indexSummaryTableItem, true)} - - ); - - expect(screen.getByLabelText(CHECK_INDEX)).toBeDisabled(); - expect(screen.getByLabelText('Loading')).toBeInTheDocument(); - }); - - test('it invokes the `onExpandAction` with the index name when the view check details button is clicked', async () => { - const onExpandAction = jest.fn(); + test('it invokes the `onViewHistoryAction` with the index name when the view history button is clicked', async () => { + const onViewHistoryAction = jest.fn(); const columns = getSummaryTableColumns({ formatBytes, @@ -207,13 +178,12 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction, - checkState: getCheckState(indexName), + onViewHistoryAction, }); const expandActionRender = ( (columns[0] as EuiTableActionsColumnType) - .actions[0] as CustomItemAction + .actions[1] as CustomItemAction ).render; render( @@ -222,10 +192,10 @@ describe('helpers', () => { ); - const button = screen.getByLabelText(VIEW_CHECK_DETAILS); + const button = screen.getByLabelText(VIEW_HISTORY); await userEvent.click(button); - expect(onExpandAction).toBeCalledWith(indexSummaryTableItem.indexName); + expect(onViewHistoryAction).toBeCalledWith(indexSummaryTableItem.indexName); }); }); @@ -242,8 +212,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType @@ -266,8 +235,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType @@ -294,8 +262,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType @@ -319,8 +286,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const indexNameRender = (columns[2] as EuiTableFieldDataColumnType) .render; @@ -344,8 +310,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const docsCountRender = (columns[3] as EuiTableFieldDataColumnType) .render; @@ -380,8 +345,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const incompatibleRender = ( columns[4] as EuiTableFieldDataColumnType @@ -403,8 +367,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const incompatibleRender = ( columns[4] as EuiTableFieldDataColumnType @@ -461,8 +424,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; @@ -488,8 +450,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; @@ -514,8 +475,7 @@ describe('helpers', () => { isILMAvailable: false, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; @@ -538,8 +498,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType) @@ -563,8 +522,7 @@ describe('helpers', () => { isILMAvailable, pattern: 'auditbeat-*', onCheckNowAction: jest.fn(), - onExpandAction: jest.fn(), - checkState: getCheckState(indexName), + onViewHistoryAction: jest.fn(), }); const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType) diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx index 8cdac8e35d1b3..c930d47babc2e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx @@ -33,10 +33,10 @@ import { SIZE, } from '../../../../../translations'; import * as i18n from '../translations'; -import { UseIndicesCheckCheckState } from '../../../../../hooks/use_indices_check/types'; import { IndexResultBadge } from '../../index_result_badge'; import { Stat } from '../../../../../stat'; import { getIndexResultToolTip } from '../../utils/get_index_result_tooltip'; +import { CHECK_NOW } from '../../translations'; const ProgressContainer = styled.div` width: 150px; @@ -100,17 +100,15 @@ export const getSummaryTableColumns = ({ formatNumber, isILMAvailable, pattern, - onExpandAction, onCheckNowAction, - checkState, + onViewHistoryAction, }: { formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; isILMAvailable: boolean; pattern: string; - onExpandAction: (indexName: string) => void; onCheckNowAction: (indexName: string) => void; - checkState: UseIndicesCheckCheckState; + onViewHistoryAction: (indexName: string) => void; }): Array> => [ { name: i18n.ACTIONS, @@ -118,30 +116,28 @@ export const getSummaryTableColumns = ({ width: '65px', actions: [ { - name: i18n.VIEW_CHECK_DETAILS, + name: CHECK_NOW, render: (item) => { return ( - + onExpandAction(item.indexName)} + iconType="refresh" + aria-label={CHECK_NOW} + onClick={() => onCheckNowAction(item.indexName)} /> ); }, }, { - name: i18n.CHECK_INDEX, + name: i18n.VIEW_HISTORY, render: (item) => { - const isChecking = checkState[item.indexName]?.isChecking ?? false; return ( - + onCheckNowAction(item.indexName)} + iconType="clockCounter" + aria-label={i18n.VIEW_HISTORY} + onClick={() => onViewHistoryAction(item.indexName)} /> ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts index 8e13a4be4cc30..f891f7ccdc7b8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts @@ -43,3 +43,18 @@ export const THIS_INDEX_HAS_NOT_BEEN_CHECKED = i18n.translate( defaultMessage: 'This index has not been checked', } ); + +export const FAIL = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.fail', { + defaultMessage: 'Fail', +}); + +export const PASS = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.pass', { + defaultMessage: 'Pass', +}); + +export const CHECK_NOW: string = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.checkNow', + { + defaultMessage: 'Check now', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.test.ts similarity index 61% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.test.ts index 68a5da877da9a..5646fe62663c5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.test.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { getIndexResultBadgeColor } from './get_index_result_badge_color'; +import { getCheckTextColor } from './get_check_text_color'; -describe('getIndexResultBadgeColor', () => { +describe('getCheckTextColor', () => { test('it returns `ghost` when `incompatible` is undefined', () => { - expect(getIndexResultBadgeColor(undefined)).toEqual('ghost'); + expect(getCheckTextColor(undefined)).toEqual('ghost'); }); test('it returns `success` when `incompatible` is zero', () => { - expect(getIndexResultBadgeColor(0)).toEqual('#6dcbb1'); + expect(getCheckTextColor(0)).toEqual('#6dcbb1'); }); test('it returns `danger` when `incompatible` is NOT zero', () => { - expect(getIndexResultBadgeColor(1)).toEqual('danger'); + expect(getCheckTextColor(1)).toEqual('danger'); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.ts similarity index 81% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.ts index 61f3aaa89a372..991c49f14cea5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_check_text_color.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const getIndexResultBadgeColor = (incompatible: number | undefined): string => { +export const getCheckTextColor = (incompatible: number | undefined): string => { if (incompatible == null) { return 'ghost'; } else if (incompatible === 0) { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts index 306c9f93d83b6..03edd859261e3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts @@ -6,12 +6,12 @@ */ import { omit } from 'lodash/fp'; -import { mockStatsAuditbeatIndex } from '../../../../mock/stats/mock_stats_packetbeat_index'; -import { mockStatsPacketbeatIndex } from '../../../../mock/stats/mock_stats_auditbeat_index'; import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; import { mockStats } from '../../../../mock/stats/mock_stats'; import { getIndexNames, getPatternDocsCount, getPatternSizeInBytes } from './stats'; import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { mockStatsPacketbeatIndex } from '../../../../mock/stats/mock_stats_packetbeat_index'; +import { mockStatsAuditbeatIndex } from '../../../../mock/stats/mock_stats_auditbeat_index'; describe('getIndexNames', () => { const isILMAvailable = true; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx index 23a4fc46f6ad4..132bc52d16d66 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx @@ -9,12 +9,13 @@ import React, { useCallback, useMemo } from 'react'; import { useResultsRollupContext } from '../../contexts/results_rollup_context'; import { StorageTreemap } from './storage_treemap'; -import { DEFAULT_MAX_CHART_HEIGHT } from '../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { SelectedIndex } from '../../types'; import { useDataQualityContext } from '../../data_quality_context'; import { DOCS_UNIT } from './translations'; import { getFlattenedBuckets } from './utils/get_flattened_buckets'; +export const DEFAULT_MAX_CHART_HEIGHT = 300; // px + export interface Props { onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx index 8cd41626dfc28..9ed31d89c37af 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx @@ -21,12 +21,12 @@ import { } from '../../../mock/test_providers/test_providers'; import type { Props } from '.'; import { StorageTreemap } from '.'; -import { DEFAULT_MAX_CHART_HEIGHT } from '../../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { NO_DATA_LABEL } from './translations'; import { PatternRollup } from '../../../types'; import { FlattenedBucket } from '../types'; import { getFlattenedBuckets } from '../utils/get_flattened_buckets'; import { getLegendItems } from './utils/get_legend_items'; +import { DEFAULT_MAX_CHART_HEIGHT } from '..'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx index 5a0a7be5bab3d..469f42b648ba2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx @@ -20,14 +20,11 @@ import type { import { Chart, Partition, PartitionLayout, Settings } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; - import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; + import { ChartLegendItem } from './chart_legend_item'; import { NoData } from './no_data'; -import { - ChartFlexItem, - LegendContainer, -} from '../../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { PatternRollup, SelectedIndex } from '../../../types'; import { useDataQualityContext } from '../../../data_quality_context'; import { FlattenedBucket } from '../types'; @@ -35,6 +32,25 @@ import { getPathToFlattenedBucketMap } from './utils/get_path_to_flattened_bucke import { getLayersMultiDimensional } from './utils/get_layers_multi_dimensional'; import { getLegendItems } from './utils/get_legend_items'; +export const ChartFlexItem = styled(EuiFlexItem)<{ + $maxChartHeight: number | undefined; + $minChartHeight: number; +}>` + ${({ $maxChartHeight }) => ($maxChartHeight != null ? `max-height: ${$maxChartHeight}px;` : '')} + min-height: ${({ $minChartHeight }) => `${$minChartHeight}px`}; +`; + +export const LegendContainer = styled.div<{ + $height?: number; + $width?: number; +}>` + margin-left: ${({ theme }) => theme.eui.euiSizeM}; + margin-top: ${({ theme }) => theme.eui.euiSizeM}; + ${({ $height }) => ($height != null ? `height: ${$height}px;` : '')} + scrollbar-width: thin; + ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} +`; + export const DEFAULT_MIN_CHART_HEIGHT = 240; // px export const LEGEND_WIDTH = 220; // px export const LEGEND_TEXT_WITH = 120; // px diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx index 9e90b28e71a9e..7f8405845714e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButton } from '@elastic/eui'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; @@ -16,6 +16,7 @@ import { checkIndex } from '../../../utils/check_index'; import { useDataQualityContext } from '../../../data_quality_context'; import * as i18n from '../../../translations'; import type { IndexToCheck } from '../../../types'; +import { useAbortControllerRef } from '../../../hooks/use_abort_controller_ref'; const CheckAllButton = styled(EuiButton)` width: 112px; @@ -50,20 +51,26 @@ const CheckAllComponent: React.FC = ({ const { httpFetch, isILMAvailable, formatBytes, formatNumber, ilmPhases, patterns } = useDataQualityContext(); const { onCheckCompleted, patternIndexNames } = useResultsRollupContext(); - const abortController = useRef(new AbortController()); + const abortControllerRef = useAbortControllerRef(); const [isRunning, setIsRunning] = useState(false); const cancelIfRunning = useCallback(() => { if (isRunning) { - if (!abortController.current.signal.aborted) { + if (!abortControllerRef.current.signal.aborted) { setIndexToCheck(null); setIsRunning(false); setCheckAllIndiciesChecked(0); setCheckAllTotalIndiciesToCheck(0); - abortController.current.abort(); + abortControllerRef.current.abort(); } } - }, [isRunning, setCheckAllIndiciesChecked, setCheckAllTotalIndiciesToCheck, setIndexToCheck]); + }, [ + abortControllerRef, + isRunning, + setCheckAllIndiciesChecked, + setCheckAllTotalIndiciesToCheck, + setIndexToCheck, + ]); const onClick = useCallback(() => { async function beginCheck() { @@ -76,14 +83,14 @@ const CheckAllComponent: React.FC = ({ setCheckAllTotalIndiciesToCheck(allIndicesToCheck.length); for (const { indexName, pattern } of allIndicesToCheck) { - if (!abortController.current.signal.aborted) { + if (!abortControllerRef.current.signal.aborted) { setIndexToCheck({ indexName, pattern, }); await checkIndex({ - abortController: abortController.current, + abortController: abortControllerRef.current, batchId, checkAllStartTime: startTime, formatBytes, @@ -97,7 +104,7 @@ const CheckAllComponent: React.FC = ({ pattern, }); - if (!abortController.current.signal.aborted) { + if (!abortControllerRef.current.signal.aborted) { await wait(DELAY_AFTER_EVERY_CHECK_COMPLETES); incrementCheckAllIndiciesChecked(); checked++; @@ -105,7 +112,7 @@ const CheckAllComponent: React.FC = ({ } } - if (!abortController.current.signal.aborted) { + if (!abortControllerRef.current.signal.aborted) { setIndexToCheck(null); setIsRunning(false); } @@ -114,11 +121,12 @@ const CheckAllComponent: React.FC = ({ if (isRunning) { cancelIfRunning(); } else { - abortController.current = new AbortController(); + abortControllerRef.current = new AbortController(); setIsRunning(true); beginCheck(); } }, [ + abortControllerRef, cancelIfRunning, formatBytes, formatNumber, @@ -139,12 +147,6 @@ const CheckAllComponent: React.FC = ({ }; }, [cancelIfRunning, ilmPhases, patterns]); - useEffect(() => { - return () => { - abortController.current.abort(); - }; - }, [abortController]); - const disabled = isILMAvailable && ilmPhases.length === 0; return ( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_status/errors_popover/errors_viewer/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_status/errors_popover/errors_viewer/helpers.tsx index 8644a0a89c02a..557ed879ecfc4 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_status/errors_popover/errors_viewer/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_status/errors_popover/errors_viewer/helpers.tsx @@ -9,12 +9,11 @@ import type { EuiTableFieldDataColumnType } from '@elastic/eui'; import { EuiCode } from '@elastic/eui'; import React from 'react'; +import { EMPTY_PLACEHOLDER } from '../../../../../constants'; import type { ErrorSummary } from '../../../../../types'; import { INDEX } from '../../../../../translations'; import { ERROR, PATTERN } from '../../../translations'; -export const EMPTY_PLACEHOLDER = '--'; - export const ERRORS_CONTAINER_MAX_WIDTH = 600; // px export const ERRORS_CONTAINER_MIN_WIDTH = 450; // px diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx index 88d6e42af97ac..e155bfadea07d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx @@ -79,7 +79,7 @@ export const getAllMarkdownCommentsFromResults = ({ formatNumber, ilmPhase: item.ilmPhase, indexName: item.indexName, - incompatible: result?.incompatible, + incompatibleFieldsCount: result?.incompatible, isILMAvailable, patternDocsCount: patternRollup.docsCount ?? 0, sizeInBytes, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.test.tsx new file mode 100644 index 0000000000000..98e617ca3a909 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useAbortControllerRef } from '.'; + +describe('useAbortControllerRef', () => { + describe('when the hook is rendered', () => { + it('should return a ref object with an AbortController instance', () => { + const { result } = renderHook(() => useAbortControllerRef()); + + expect(result.current.current).not.toBeNull(); + expect(result.current.current).toBeInstanceOf(AbortController); + }); + }); + + describe('when the hook is rerendered', () => { + it('should return the same instance of AbortController', () => { + const { result, rerender } = renderHook(() => useAbortControllerRef()); + + const initialController = result.current.current; + + rerender(); + + expect(result.current.current).toBe(initialController); + }); + }); + + describe('when the hook is re-mounted', () => { + it('should return a new instance of AbortController', () => { + const { result, rerender } = renderHook(() => useAbortControllerRef()); + + const initialController = result.current.current; + + rerender(); + + expect(result.current.current).toBe(initialController); + + rerender(); + + expect(result.current.current).toBe(initialController); + }); + }); + + describe('when the hook is unmounted', () => { + it('should abort the controller', () => { + const { result, unmount } = renderHook(() => useAbortControllerRef()); + + const controller = result.current.current; + + expect(controller.signal.aborted).toBe(false); + + unmount(); + + expect(controller.signal.aborted).toBe(true); + }); + + describe('and ref was updated', () => { + it('should abort the updated controller', () => { + const { result, unmount } = renderHook(() => useAbortControllerRef()); + + const controller = result.current.current; + + expect(controller.signal.aborted).toBe(false); + + const newController = new AbortController(); + result.current.current = newController; + + unmount(); + + expect(newController.signal.aborted).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.tsx new file mode 100644 index 0000000000000..0daab6b0a6909 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_abort_controller_ref/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef } from 'react'; + +export const useAbortControllerRef = () => { + const abortControllerRef = useRef(new AbortController()); + + useEffect(() => { + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + abortControllerRef.current.abort(); + }; + }, []); + + return abortControllerRef; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_indices_check/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_indices_check/index.tsx index 6ad00aacb338d..d08073597e708 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_indices_check/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_indices_check/index.tsx @@ -12,14 +12,14 @@ import { UnallowedValuesError } from '../../utils/fetch_unallowed_values'; import { checkIndex as _checkIndex, CheckIndexProps } from '../../utils/check_index'; import { initialState, reducer } from './reducer'; import { UseIndicesCheckReturnValue } from './types'; -import { useIsMounted } from '../use_is_mounted'; +import { useIsMountedRef } from '../use_is_mounted_ref'; export const useIndicesCheck = ({ onCheckCompleted, }: { onCheckCompleted: OnCheckCompleted; }): UseIndicesCheckReturnValue => { - const { isMountedRef } = useIsMounted(); + const { isMountedRef } = useIsMountedRef(); const [state, dispatch] = useReducer(reducer, initialState); const checkIndex = useCallback( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.test.tsx similarity index 80% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.test.tsx index d29aaf8ace201..f32beafe61efe 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.test.tsx @@ -7,11 +7,11 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { useIsMounted } from '.'; +import { useIsMountedRef } from '.'; -describe('useIsMounted', () => { +describe('useIsMountedRef', () => { it('should return a ref that is true when mounted and false when unmounted', () => { - const { result, unmount } = renderHook(() => useIsMounted()); + const { result, unmount } = renderHook(() => useIsMountedRef()); expect(result.current.isMountedRef.current).toBe(true); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.tsx similarity index 88% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.tsx index 6ba5ebacf8ea0..98a617567dd5e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_is_mounted_ref/index.tsx @@ -12,7 +12,7 @@ import { MutableRefObject, useEffect, useRef } from 'react'; * * Main use case is to avoid setting state on an unmounted component. */ -export const useIsMounted = (): { isMountedRef: MutableRefObject } => { +export const useIsMountedRef = (): { isMountedRef: MutableRefObject } => { const isMountedRef = useRef(true); useEffect(() => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx index e2571b5d0a75c..28b36765a245b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx @@ -35,12 +35,12 @@ import type { TelemetryEvents, } from '../../types'; import { - getIncompatibleMappingsFields, - getIncompatibleValuesFields, - getSameFamilyFields, -} from '../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers'; + getEscapedIncompatibleMappingsFields, + getEscapedIncompatibleValuesFields, + getEscapedSameFamilyFields, +} from './utils/metadata'; import { UseResultsRollupReturnValue } from './types'; -import { useIsMounted } from '../use_is_mounted'; +import { useIsMountedRef } from '../use_is_mounted_ref'; import { getDocsCount, getIndexIncompatible, getSizeInBytes } from '../../utils/stats'; import { getIlmPhase } from '../../utils/get_ilm_phase'; @@ -53,7 +53,7 @@ interface Props { isILMAvailable: boolean; } const useStoredPatternResults = (patterns: string[], toasts: IToasts, httpFetch: HttpHandler) => { - const { isMountedRef } = useIsMounted(); + const { isMountedRef } = useIsMountedRef(); const [storedPatternResults, setStoredPatternResults] = useState< Array<{ pattern: string; results: Record }> >([]); @@ -208,13 +208,13 @@ export const useResultsRollup = ({ numberOfIndices: 1, numberOfIndicesChecked: 1, numberOfSameFamily: getTotalPatternSameFamily(results), - sameFamilyFields: getSameFamilyFields(partitionedFieldMetadata.sameFamily), + sameFamilyFields: getEscapedSameFamilyFields(partitionedFieldMetadata.sameFamily), sizeInBytes: getSizeInBytes({ stats, indexName }), timeConsumedMs: requestTime, - unallowedMappingFields: getIncompatibleMappingsFields( + unallowedMappingFields: getEscapedIncompatibleMappingsFields( partitionedFieldMetadata.incompatible ), - unallowedValueFields: getIncompatibleValuesFields( + unallowedValueFields: getEscapedIncompatibleValuesFields( partitionedFieldMetadata.incompatible ), }; @@ -253,17 +253,34 @@ export const useResultsRollup = ({ setPatternIndexNames({}); }, [ilmPhases, patterns]); - return { - onCheckCompleted, - patternIndexNames, - patternRollups, - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - totalSameFamily, - totalSizeInBytes, - updatePatternIndexNames, - updatePatternRollup, - }; + const useResultsRollupReturnValue = useMemo( + () => ({ + onCheckCompleted, + patternIndexNames, + patternRollups, + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + totalSameFamily, + totalSizeInBytes, + updatePatternIndexNames, + updatePatternRollup, + }), + [ + onCheckCompleted, + patternIndexNames, + patternRollups, + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + totalSameFamily, + totalSizeInBytes, + updatePatternIndexNames, + updatePatternRollup, + ] + ); + + return useResultsRollupReturnValue; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/types.ts index 093ee8fdd645c..a1e9a27418a89 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/types.ts @@ -5,14 +5,7 @@ * 2.0. */ -import { - IlmPhase, - IncompatibleFieldMappingItem, - IncompatibleFieldValueItem, - OnCheckCompleted, - PatternRollup, - SameFamilyFieldItem, -} from '../../types'; +import { OnCheckCompleted, PatternRollup } from '../../types'; export interface UseResultsRollupReturnValue { onCheckCompleted: OnCheckCompleted; @@ -33,29 +26,3 @@ export interface UseResultsRollupReturnValue { }) => void; updatePatternRollup: (patternRollup: PatternRollup) => void; } - -export interface StorageResult { - batchId: string; - indexName: string; - indexPattern: string; - isCheckAll: boolean; - checkedAt: number; - docsCount: number; - totalFieldCount: number; - ecsFieldCount: number; - customFieldCount: number; - incompatibleFieldCount: number; - incompatibleFieldMappingItems: IncompatibleFieldMappingItem[]; - incompatibleFieldValueItems: IncompatibleFieldValueItem[]; - sameFamilyFieldCount: number; - sameFamilyFields: string[]; - sameFamilyFieldItems: SameFamilyFieldItem[]; - unallowedMappingFields: string[]; - unallowedValueFields: string[]; - sizeInBytes: number; - ilmPhase?: IlmPhase; - markdownComments: string[]; - ecsVersion: string; - indexId: string; - error: string | null; -} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.test.ts index f6cd702ef139f..4ab545db53dda 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.test.ts @@ -22,394 +22,392 @@ const defaultNumberFormat = '0,0.[000]'; const formatNumber = (value: number | undefined) => value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; -describe('helpers', () => { - let originalFetch: (typeof global)['fetch']; +let originalFetch: (typeof global)['fetch']; - beforeAll(() => { - originalFetch = global.fetch; - }); +beforeAll(() => { + originalFetch = global.fetch; +}); - afterAll(() => { - global.fetch = originalFetch; - }); +afterAll(() => { + global.fetch = originalFetch; +}); - describe('updateResultOnCheckCompleted', () => { - const packetbeatStats861: MeteringStatsIndex = - mockPacketbeatPatternRollup.stats != null - ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.6.1-2023.02.04-000001'] - : ({} as MeteringStatsIndex); - const packetbeatStats853: MeteringStatsIndex = - mockPacketbeatPatternRollup.stats != null - ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.5.3-2023.02.04-000001'] - : ({} as MeteringStatsIndex); +describe('updateResultOnCheckCompleted', () => { + const packetbeatStats861: MeteringStatsIndex = + mockPacketbeatPatternRollup.stats != null + ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.6.1-2023.02.04-000001'] + : ({} as MeteringStatsIndex); + const packetbeatStats853: MeteringStatsIndex = + mockPacketbeatPatternRollup.stats != null + ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.5.3-2023.02.04-000001'] + : ({} as MeteringStatsIndex); - test('it returns the updated rollups', () => { - expect( - getPatternRollupsWithLatestCheckResult({ - error: null, - formatBytes, - formatNumber, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'packetbeat-*', - patternRollups: { - 'packetbeat-*': mockPacketbeatPatternRollup, - }, - }) - ).toEqual({ - 'packetbeat-*': { - docsCount: 3258632, - error: null, - ilmExplain: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - index: '.ds-packetbeat-8.6.1-2023.02.04-000001', - managed: true, - policy: 'packetbeat', - index_creation_date_millis: 1675536751379, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536751379, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536751809, - action: 'rollover', - action_time_millis: 1675536751809, - step: 'check-rollover-ready', - step_time_millis: 1675536751809, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, - }, - '.ds-packetbeat-8.5.3-2023.02.04-000001': { - index: '.ds-packetbeat-8.5.3-2023.02.04-000001', - managed: true, + test('it returns the updated rollups', () => { + expect( + getPatternRollupsWithLatestCheckResult({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + isILMAvailable: true, + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': mockPacketbeatPatternRollup, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { policy: 'packetbeat', - index_creation_date_millis: 1675536774084, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536774084, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536774416, - action: 'rollover', - action_time_millis: 1675536774416, - step: 'check-rollover-ready', - step_time_millis: 1675536774416, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, + version: 1, + modified_date_in_millis: 1675536751205, }, }, - ilmExplainPhaseCounts: { - hot: 2, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 0, - }, - indices: 2, - pattern: 'packetbeat-*', - results: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - docsCount: 1628343, - error: null, - ilmPhase: 'hot', - incompatible: 3, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - markdownComments: [ - '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | `hot` | 697.7MB |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, - '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', - ], - pattern: 'packetbeat-*', - sameFamily: 0, - checkedAt: expect.any(Number), + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, }, }, - sizeInBytes: 1464758182, - stats: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, - '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | `hot` | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + sameFamily: 0, + checkedAt: expect.any(Number), }, }, - }); + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, }); + }); - test('it returns the expected results when `patternRollup` does NOT have a `docsCount`', () => { - const noDocsCount = { - ...mockPacketbeatPatternRollup, - docsCount: undefined, // <-- - }; + test('it returns the expected results when `patternRollup` does NOT have a `docsCount`', () => { + const noDocsCount = { + ...mockPacketbeatPatternRollup, + docsCount: undefined, // <-- + }; - expect( - getPatternRollupsWithLatestCheckResult({ - error: null, - formatBytes, - formatNumber, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'packetbeat-*', - patternRollups: { - 'packetbeat-*': noDocsCount, - }, - }) - ).toEqual({ - 'packetbeat-*': { - docsCount: undefined, // <-- - error: null, - ilmExplain: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - index: '.ds-packetbeat-8.6.1-2023.02.04-000001', - managed: true, - policy: 'packetbeat', - index_creation_date_millis: 1675536751379, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536751379, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536751809, - action: 'rollover', - action_time_millis: 1675536751809, - step: 'check-rollover-ready', - step_time_millis: 1675536751809, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, - }, - '.ds-packetbeat-8.5.3-2023.02.04-000001': { - index: '.ds-packetbeat-8.5.3-2023.02.04-000001', - managed: true, + expect( + getPatternRollupsWithLatestCheckResult({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + isILMAvailable: true, + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': noDocsCount, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: undefined, // <-- + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { policy: 'packetbeat', - index_creation_date_millis: 1675536774084, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536774084, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536774416, - action: 'rollover', - action_time_millis: 1675536774416, - step: 'check-rollover-ready', - step_time_millis: 1675536774416, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, + version: 1, + modified_date_in_millis: 1675536751205, }, }, - ilmExplainPhaseCounts: { - hot: 2, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 0, - }, - indices: 2, - pattern: 'packetbeat-*', - results: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - docsCount: 1628343, - error: null, - ilmPhase: 'hot', - incompatible: 3, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - markdownComments: [ - '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 () | 3 | `hot` | 697.7MB |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, - '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', - ], - pattern: 'packetbeat-*', - sameFamily: 0, - checkedAt: expect.any(Number), + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, }, }, - sizeInBytes: 1464758182, - stats: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, - '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 | 3 | `hot` | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + sameFamily: 0, + checkedAt: expect.any(Number), }, }, - }); + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, }); + }); - test('it returns the expected results when `partitionedFieldMetadata` is null', () => { - expect( - getPatternRollupsWithLatestCheckResult({ - error: null, - formatBytes, - formatNumber, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - isILMAvailable: true, - partitionedFieldMetadata: null, // <-- - pattern: 'packetbeat-*', - patternRollups: { - 'packetbeat-*': mockPacketbeatPatternRollup, - }, - }) - ).toEqual({ - 'packetbeat-*': { - docsCount: 3258632, - error: null, - ilmExplain: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - index: '.ds-packetbeat-8.6.1-2023.02.04-000001', - managed: true, - policy: 'packetbeat', - index_creation_date_millis: 1675536751379, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536751379, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536751809, - action: 'rollover', - action_time_millis: 1675536751809, - step: 'check-rollover-ready', - step_time_millis: 1675536751809, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, - }, - '.ds-packetbeat-8.5.3-2023.02.04-000001': { - index: '.ds-packetbeat-8.5.3-2023.02.04-000001', - managed: true, + test('it returns the expected results when `partitionedFieldMetadata` is null', () => { + expect( + getPatternRollupsWithLatestCheckResult({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + isILMAvailable: true, + partitionedFieldMetadata: null, // <-- + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': mockPacketbeatPatternRollup, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { policy: 'packetbeat', - index_creation_date_millis: 1675536774084, - time_since_index_creation: '25.26d', - lifecycle_date_millis: 1675536774084, - age: '25.26d', - phase: 'hot', - phase_time_millis: 1675536774416, - action: 'rollover', - action_time_millis: 1675536774416, - step: 'check-rollover-ready', - step_time_millis: 1675536774416, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, + version: 1, + modified_date_in_millis: 1675536751205, }, }, - ilmExplainPhaseCounts: { - hot: 2, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 0, - }, - indices: 2, - pattern: 'packetbeat-*', - results: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - docsCount: 1628343, - error: null, - ilmPhase: 'hot', - incompatible: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - markdownComments: [], - pattern: 'packetbeat-*', - checkedAt: undefined, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, }, }, - sizeInBytes: 1464758182, - stats: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, - '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [], + pattern: 'packetbeat-*', + checkedAt: undefined, }, }, - }); + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, }); + }); - test('it returns the updated rollups when there is no `partitionedFieldMetadata`', () => { - const noIlmExplain = { - ...mockPacketbeatPatternRollup, - ilmExplain: null, - }; + test('it returns the updated rollups when there is no `partitionedFieldMetadata`', () => { + const noIlmExplain = { + ...mockPacketbeatPatternRollup, + ilmExplain: null, + }; - expect( - getPatternRollupsWithLatestCheckResult({ - error: null, - formatBytes, - formatNumber, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'packetbeat-*', - patternRollups: { - 'packetbeat-*': noIlmExplain, - }, - }) - ).toEqual({ - 'packetbeat-*': { - docsCount: 3258632, - error: null, - ilmExplain: null, - ilmExplainPhaseCounts: { - hot: 2, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 0, - }, - indices: 2, - pattern: 'packetbeat-*', - results: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - docsCount: 1628343, - error: null, - ilmPhase: undefined, - incompatible: 3, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - markdownComments: [ - '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | -- | 697.7MB |\n\n', - '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, - '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', - ], - pattern: 'packetbeat-*', - sameFamily: 0, - checkedAt: expect.any(Number), - }, - }, - sizeInBytes: 1464758182, - stats: { - '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, - '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + expect( + getPatternRollupsWithLatestCheckResult({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + isILMAvailable: true, + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': noIlmExplain, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: null, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: undefined, + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | -- | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + sameFamily: 0, + checkedAt: expect.any(Number), }, }, - }); + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, }); + }); - test('it returns the unmodified rollups when `pattern` is not a member of `patternRollups`', () => { - const shouldNotBeModified: Record = { - 'packetbeat-*': mockPacketbeatPatternRollup, - }; + test('it returns the unmodified rollups when `pattern` is not a member of `patternRollups`', () => { + const shouldNotBeModified: Record = { + 'packetbeat-*': mockPacketbeatPatternRollup, + }; - expect( - getPatternRollupsWithLatestCheckResult({ - error: null, - formatBytes, - formatNumber, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - isILMAvailable: true, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'this-pattern-is-not-in-pattern-rollups', // <-- - patternRollups: shouldNotBeModified, - }) - ).toEqual(shouldNotBeModified); - }); + expect( + getPatternRollupsWithLatestCheckResult({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + isILMAvailable: true, + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'this-pattern-is-not-in-pattern-rollups', // <-- + patternRollups: shouldNotBeModified, + }) + ).toEqual(shouldNotBeModified); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts index e826730df89cc..7f7f671057c26 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts @@ -5,11 +5,15 @@ * 2.0. */ -import { getAllIncompatibleMarkdownComments } from '../../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers'; -import { getSizeInBytes } from '../../../utils/stats'; import type { IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../../../types'; import { getIndexDocsCountFromRollup } from './stats'; import { getIlmPhase } from '../../../utils/get_ilm_phase'; +import { getSizeInBytes } from '../../../utils/stats'; +import { + getAllIncompatibleMarkdownComments, + getIncompatibleMappings, + getIncompatibleValues, +} from '../../../utils/markdown'; export const getPatternRollupsWithLatestCheckResult = ({ error, @@ -47,20 +51,35 @@ export const getPatternRollupsWithLatestCheckResult = ({ const sizeInBytes = getSizeInBytes({ indexName, stats: patternRollup.stats }); - const markdownComments = - partitionedFieldMetadata != null - ? getAllIncompatibleMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }) - : []; + let markdownComments: string[] = []; + + if (partitionedFieldMetadata != null) { + const incompatibleMappingsFields = getIncompatibleMappings( + partitionedFieldMetadata.incompatible + ); + const incompatibleValuesFields = getIncompatibleValues(partitionedFieldMetadata.incompatible); + const sameFamilyFieldsCount = partitionedFieldMetadata.sameFamily.length; + const ecsCompliantFieldsCount = partitionedFieldMetadata.ecsCompliant.length; + const customFieldsCount = partitionedFieldMetadata.custom.length; + const allFieldsCount = partitionedFieldMetadata.all.length; + + markdownComments = getAllIncompatibleMarkdownComments({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleMappingsFields, + incompatibleValuesFields, + sameFamilyFieldsCount, + ecsCompliantFieldsCount, + customFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, + }); + } const incompatible = partitionedFieldMetadata?.incompatible.length; const sameFamily = partitionedFieldMetadata?.sameFamily.length; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.test.ts new file mode 100644 index 0000000000000..6b819f2f3b240 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { mockPartitionedFieldMetadataWithSameFamily } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; +import { + getEscapedIncompatibleMappingsFields, + getEscapedIncompatibleValuesFields, + getEscapedSameFamilyFields, +} from './metadata'; + +describe('getEscapedIncompatibleMappingsFields', () => { + test('it (only) returns incompatible mapping fields', () => { + expect(getEscapedIncompatibleMappingsFields(mockPartitionedFieldMetadata.incompatible)).toEqual( + ['host.name', 'source.ip'] + ); + }); + + test('it escapes newlines from the field names', () => { + const fieldWithNewlines = { + ...mockPartitionedFieldMetadata.incompatible[1], + indexFieldName: 'host.name\nhost.name2', + }; + expect(getEscapedIncompatibleMappingsFields([fieldWithNewlines])).toEqual([ + 'host.name host.name2', + ]); + }); +}); + +describe('getEscapedIncompatibleValuesFields', () => { + test('it (only) returns incompatible values fields', () => { + expect(getEscapedIncompatibleValuesFields(mockPartitionedFieldMetadata.incompatible)).toEqual([ + 'event.category', + ]); + }); + + test('it escapes newlines from the field names', () => { + const fieldWithNewlines = { + ...mockPartitionedFieldMetadata.incompatible[0], + indexFieldName: 'event.category\nhost.name', + }; + expect(getEscapedIncompatibleValuesFields([fieldWithNewlines])).toEqual([ + 'event.category host.name', + ]); + }); +}); + +describe('getEscapedSameFamilyFields', () => { + test('it returns same family fields with escaped newlines in field names', () => { + const fieldWithNewlines = { + ...mockPartitionedFieldMetadataWithSameFamily.sameFamily[0], + indexFieldName: 'event.category\nhost.name', + }; + expect(getEscapedSameFamilyFields([fieldWithNewlines])).toEqual(['event.category host.name']); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.ts new file mode 100644 index 0000000000000..5aea486e4033d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/metadata.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SameFamilyFieldMetadata, IncompatibleFieldMetadata } from '../../../types'; +import { + escapeNewlines, + getIncompatibleMappings, + getIncompatibleValues, +} from '../../../utils/markdown'; + +export const getEscapedIncompatibleMappingsFields = ( + incompatibleFields: IncompatibleFieldMetadata[] +): string[] => + getIncompatibleMappings(incompatibleFields).map((x) => escapeNewlines(x.indexFieldName)); + +export const getEscapedIncompatibleValuesFields = ( + incompatibleFields: IncompatibleFieldMetadata[] +): string[] => + getIncompatibleValues(incompatibleFields).map((x) => escapeNewlines(x.indexFieldName)); + +export const getEscapedSameFamilyFields = (sameFamilyFields: SameFamilyFieldMetadata[]): string[] => + sameFamilyFields.map((x) => escapeNewlines(x.indexFieldName)); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts index 0e894694ed1e9..9f315d65c01d5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts @@ -6,10 +6,11 @@ */ import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; + import { mockPartitionedFieldMetadataWithSameFamily } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; -import { StorageResult } from '../types'; import { formatStorageResult, getStorageResults, postStorageResult } from './storage'; -import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; +import { StorageResult } from '../../../types'; describe('formatStorageResult', () => { it('should correctly format the input data into a StorageResult object', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts index b7b3b120441d3..e4a5c43d5b4a5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts @@ -15,8 +15,8 @@ import { IncompatibleFieldValueItem, PartitionedFieldMetadata, SameFamilyFieldItem, + StorageResult, } from '../../../types'; -import { StorageResult } from '../types'; import { GET_INDEX_RESULTS_LATEST, POST_INDEX_RESULTS } from '../constants'; import { INTERNAL_API_VERSION } from '../../../constants'; import { GET_RESULTS_ERROR_TITLE, POST_RESULT_ERROR_TITLE } from '../../../translations'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx index b7ea6613dac62..90e5dba08d4dc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { TestExternalProviders } from './mock/test_providers/test_providers'; import { mockUseResultsRollup } from './mock/use_results_rollup/mock_use_results_rollup'; -import { getCheckState } from './stub/get_check_state'; +import { getCheckStateStub } from './stub/get_check_state_stub'; import * as useResultsRollup from './hooks/use_results_rollup'; import * as useIndicesCheck from './hooks/use_indices_check'; import { DataQualityPanel } from '.'; @@ -38,7 +38,7 @@ jest.spyOn(useResultsRollup, 'useResultsRollup').mockImplementation(() => mockUs jest.spyOn(useIndicesCheck, 'useIndicesCheck').mockImplementation(() => ({ checkIndex: jest.fn(), checkState: { - ...getCheckState('auditbeat-*'), + ...getCheckStateStub('auditbeat-*'), }, })); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/enriched_field_metadata/mock_enriched_field_metadata.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/enriched_field_metadata/mock_enriched_field_metadata.ts index 351d0bb06310f..6909428cec134 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/enriched_field_metadata/mock_enriched_field_metadata.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/enriched_field_metadata/mock_enriched_field_metadata.ts @@ -5,9 +5,14 @@ * 2.0. */ -import { CustomFieldMetadata, EcsBasedFieldMetadata } from '../../types'; +import { + CustomFieldMetadata, + EcsCompliantFieldMetadata, + IncompatibleFieldMetadata, + SameFamilyFieldMetadata, +} from '../../types'; -export const timestamp: EcsBasedFieldMetadata = { +export const timestamp: EcsCompliantFieldMetadata = { dashed_name: 'timestamp', description: 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', @@ -27,7 +32,7 @@ export const timestamp: EcsBasedFieldMetadata = { isInSameFamily: false, // `date` is not a member of any families }; -export const eventCategory: EcsBasedFieldMetadata = { +export const eventCategory: EcsCompliantFieldMetadata = { allowed_values: [ { description: @@ -166,7 +171,7 @@ export const eventCategory: EcsBasedFieldMetadata = { isInSameFamily: false, }; -export const eventCategoryWithUnallowedValues: EcsBasedFieldMetadata = { +export const eventCategoryWithUnallowedValues: IncompatibleFieldMetadata = { ...eventCategory, indexInvalidValues: [ { @@ -181,7 +186,7 @@ export const eventCategoryWithUnallowedValues: EcsBasedFieldMetadata = { isEcsCompliant: false, // because this index has unallowed values }; -export const hostNameWithTextMapping: EcsBasedFieldMetadata = { +export const hostNameWithTextMapping: IncompatibleFieldMetadata = { dashed_name: 'host-name', description: 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', @@ -227,7 +232,7 @@ export const someFieldKeyword: CustomFieldMetadata = { isInSameFamily: false, // custom fields are never in the same family }; -export const sourceIpWithTextMapping: EcsBasedFieldMetadata = { +export const sourceIpWithTextMapping: IncompatibleFieldMetadata = { dashed_name: 'source-ip', description: 'IP address of the source (IPv4 or IPv6).', flat_name: 'source.ip', @@ -253,7 +258,7 @@ export const sourceIpKeyword: CustomFieldMetadata = { isInSameFamily: false, // custom fields are never in the same family }; -export const sourcePort: EcsBasedFieldMetadata = { +export const sourcePort: EcsCompliantFieldMetadata = { dashed_name: 'source-port', description: 'Port of the source.', flat_name: 'source.port', @@ -306,7 +311,7 @@ export const mockCustomFields: CustomFieldMetadata[] = [ }, ]; -export const mockIncompatibleMappings: EcsBasedFieldMetadata[] = [ +export const mockIncompatibleMappings: IncompatibleFieldMetadata[] = [ { dashed_name: 'host-name', description: @@ -342,3 +347,47 @@ export const mockIncompatibleMappings: EcsBasedFieldMetadata[] = [ isInSameFamily: false, }, ]; + +export const mockAgentTypeSameFamilyField: SameFamilyFieldMetadata = { + dashed_name: 'agent-type', + description: + 'Type of the agent.\nThe agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + example: 'filebeat', + flat_name: 'agent.type', + ignore_above: 1024, + level: 'core', + name: 'type', + normalize: [], + short: 'Type of the agent.', + type: 'keyword', + indexFieldName: 'agent.type', + indexFieldType: 'constant_keyword', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, +}; + +export const mockHostNameSameFamilyField: SameFamilyFieldMetadata = { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + ignore_above: 1024, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + indexFieldName: 'host.name', + indexFieldType: 'constant_keyword', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, +}; + +export const mockSameFamilyFields: SameFamilyFieldMetadata[] = [ + mockAgentTypeSameFamilyField, + mockHostNameSameFamilyField, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/historical_results/mock_historical_results_response.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/historical_results/mock_historical_results_response.ts new file mode 100644 index 0000000000000..1b7c9e332f399 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/historical_results/mock_historical_results_response.ts @@ -0,0 +1,364 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { NonLegacyHistoricalResult } from '../../types'; + +export const mockHistoricalResult: NonLegacyHistoricalResult = { + batchId: 'b483fd8b-f46e-4db4-a419-f12214d9967f', + indexName: '.ds-.kibana-event-log-ds-2024.09.03-000003', + indexPattern: '.kibana-event-log-*', + isCheckAll: false, + checkedAt: 1727215052075, + docsCount: 618675, + totalFieldCount: 112, + ecsFieldCount: 44, + customFieldCount: 64, + incompatibleFieldCount: 1, + incompatibleFieldMappingItems: [], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'api', + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'library', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { + name: 'siem', + count: 110616, + }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 3, + sameFamilyFields: ['error.message', 'error.stack_trace', 'message'], + sameFamilyFieldItems: [ + { + fieldName: 'error.message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: 'Error message.', + }, + { + fieldName: 'error.stack_trace', + expectedValue: 'wildcard', + actualValue: 'keyword', + description: 'The stack trace of this error in plain text.', + }, + { + fieldName: 'message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer.\nFor structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event.\nIf multiple messages exist, they can be combined into one message.', + }, + ], + unallowedMappingFields: [], + unallowedValueFields: ['event.category'], + sizeInBytes: 85161596, + ilmPhase: 'unmanaged', + markdownComments: [ + '### .ds-.kibana-event-log-ds-2024.09.03-000003\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-.kibana-event-log-ds-2024.09.03-000003 | 618,675 (29.4%) | 1 | `unmanaged` | 81.2MB |\n\n', + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n\n#### Incompatible field values - .ds-.kibana-event-log-ds-2024.09.03-000003\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `api`, `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `library`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `siem` (110616) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'TUdSmpXrSNGeRlZqjw7L2A', + error: null, + '@timestamp': 1727215052173, + checkedBy: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', +}; + +export const mockHistoricalResultsResponse = { + data: [ + mockHistoricalResult, + { + batchId: '5cf6228f-f13e-4a6d-82ae-08fcbb077e05', + indexName: '.ds-.kibana-event-log-ds-2024.09.03-000003', + indexPattern: '.kibana-event-log-*', + isCheckAll: false, + checkedAt: 1727215042439, + docsCount: 618675, + totalFieldCount: 112, + ecsFieldCount: 44, + customFieldCount: 64, + incompatibleFieldCount: 1, + incompatibleFieldMappingItems: [], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'api', + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'library', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { + name: 'siem', + count: 110616, + }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 3, + sameFamilyFields: ['error.message', 'error.stack_trace', 'message'], + sameFamilyFieldItems: [ + { + fieldName: 'error.message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: 'Error message.', + }, + { + fieldName: 'error.stack_trace', + expectedValue: 'wildcard', + actualValue: 'keyword', + description: 'The stack trace of this error in plain text.', + }, + { + fieldName: 'message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer.\nFor structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event.\nIf multiple messages exist, they can be combined into one message.', + }, + ], + unallowedMappingFields: [], + unallowedValueFields: ['event.category'], + sizeInBytes: 85161596, + ilmPhase: 'unmanaged', + markdownComments: [ + '### .ds-.kibana-event-log-ds-2024.09.03-000003\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-.kibana-event-log-ds-2024.09.03-000003 | 618,675 (29.4%) | 1 | `unmanaged` | 81.2MB |\n\n', + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n\n#### Incompatible field values - .ds-.kibana-event-log-ds-2024.09.03-000003\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `api`, `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `library`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `siem` (110616) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'TUdSmpXrSNGeRlZqjw7L2A', + error: null, + '@timestamp': 1727215042560, + checkedBy: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + }, + { + batchId: '929b92df-6dc2-46ca-adae-82c1d9df921c', + indexName: '.ds-.kibana-event-log-ds-2024.09.03-000003', + indexPattern: '.kibana-event-log-*', + isCheckAll: true, + checkedAt: 1727215035116, + docsCount: 618675, + totalFieldCount: 112, + ecsFieldCount: 44, + customFieldCount: 64, + incompatibleFieldCount: 1, + incompatibleFieldMappingItems: [], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'api', + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'library', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { + name: 'siem', + count: 110616, + }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 3, + sameFamilyFields: ['error.message', 'error.stack_trace', 'message'], + sameFamilyFieldItems: [ + { + fieldName: 'error.message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: 'Error message.', + }, + { + fieldName: 'error.stack_trace', + expectedValue: 'wildcard', + actualValue: 'keyword', + description: 'The stack trace of this error in plain text.', + }, + { + fieldName: 'message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer.\nFor structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event.\nIf multiple messages exist, they can be combined into one message.', + }, + ], + unallowedMappingFields: [], + unallowedValueFields: ['event.category'], + sizeInBytes: 85161596, + ilmPhase: 'unmanaged', + markdownComments: [ + '### .ds-.kibana-event-log-ds-2024.09.03-000003\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-.kibana-event-log-ds-2024.09.03-000003 | 618,675 (29.4%) | 1 | `unmanaged` | 81.2MB |\n\n', + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n\n#### Incompatible field values - .ds-.kibana-event-log-ds-2024.09.03-000003\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `api`, `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `library`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `siem` (110616) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'TUdSmpXrSNGeRlZqjw7L2A', + error: null, + '@timestamp': 1727215035160, + checkedBy: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + }, + { + batchId: '68a32b86-919c-457a-8efb-0645fb602fad', + indexName: '.ds-.kibana-event-log-ds-2024.09.03-000003', + indexPattern: '.kibana-event-log-*', + isCheckAll: true, + checkedAt: 1727215030265, + docsCount: 618675, + totalFieldCount: 112, + ecsFieldCount: 44, + customFieldCount: 64, + incompatibleFieldCount: 1, + incompatibleFieldMappingItems: [], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'api', + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'library', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { + name: 'siem', + count: 110616, + }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 3, + sameFamilyFields: ['error.message', 'error.stack_trace', 'message'], + sameFamilyFieldItems: [ + { + fieldName: 'error.message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: 'Error message.', + }, + { + fieldName: 'error.stack_trace', + expectedValue: 'wildcard', + actualValue: 'keyword', + description: 'The stack trace of this error in plain text.', + }, + { + fieldName: 'message', + expectedValue: 'match_only_text', + actualValue: 'text', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer.\nFor structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event.\nIf multiple messages exist, they can be combined into one message.', + }, + ], + unallowedMappingFields: [], + unallowedValueFields: ['event.category'], + sizeInBytes: 85161596, + ilmPhase: 'unmanaged', + markdownComments: [ + '### .ds-.kibana-event-log-ds-2024.09.03-000003\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-.kibana-event-log-ds-2024.09.03-000003 | 618,675 (29.4%) | 1 | `unmanaged` | 81.2MB |\n\n', + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n\n#### Incompatible field values - .ds-.kibana-event-log-ds-2024.09.03-000003\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `api`, `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `library`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `siem` (110616) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'TUdSmpXrSNGeRlZqjw7L2A', + error: null, + '@timestamp': 1727215030312, + checkedBy: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + }, + ], + total: 4, +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.ts new file mode 100644 index 0000000000000..de202d4e18ccc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MeteringStatsIndex } from '../../types'; + +export const mockStatsAuditbeatIndex: Record = { + 'auditbeat-custom-index-1': { + uuid: 'jRlr6H_jSAysOLZ6KynoCQ', + size_in_bytes: 28425, + name: 'auditbeat-custom-index-1', + num_docs: 4, + }, +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.tsx deleted file mode 100644 index 4794de530fc43..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_auditbeat_index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MeteringStatsIndex } from '../../types'; - -export const mockStatsPacketbeatIndex: Record = { - '.ds-packetbeat-8.6.1-2023.02.04-000001': { - uuid: 'x5Uuw4j4QM2YidHLNixCwg', - num_docs: 1628343, - size_in_bytes: 731583142, - name: '.ds-packetbeat-8.6.1-2023.02.04-000001', - }, - '.ds-packetbeat-8.5.3-2023.02.04-000001': { - uuid: 'we0vNWm2Q6iz6uHubyHS6Q', - num_docs: 1630289, - size_in_bytes: 733175040, - name: '.ds-packetbeat-8.5.3-2023.02.04-000001', - }, -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_packetbeat_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_packetbeat_index.ts index de202d4e18ccc..4794de530fc43 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_packetbeat_index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/stats/mock_stats_packetbeat_index.ts @@ -7,11 +7,17 @@ import { MeteringStatsIndex } from '../../types'; -export const mockStatsAuditbeatIndex: Record = { - 'auditbeat-custom-index-1': { - uuid: 'jRlr6H_jSAysOLZ6KynoCQ', - size_in_bytes: 28425, - name: 'auditbeat-custom-index-1', - num_docs: 4, +export const mockStatsPacketbeatIndex: Record = { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + uuid: 'x5Uuw4j4QM2YidHLNixCwg', + num_docs: 1628343, + size_in_bytes: 731583142, + name: '.ds-packetbeat-8.6.1-2023.02.04-000001', + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + uuid: 'we0vNWm2Q6iz6uHubyHS6Q', + num_docs: 1630289, + size_in_bytes: 733175040, + name: '.ds-packetbeat-8.5.3-2023.02.04-000001', }, }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index 922d9b54612a6..316355f51c537 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -24,6 +24,12 @@ import { UseResultsRollupReturnValue } from '../../hooks/use_results_rollup/type import { getMergeResultsRollupContextProps } from './utils/get_merged_results_rollup_context_props'; import { getMergedDataQualityContextProps } from './utils/get_merged_data_quality_context_props'; import { getMergedIndicesCheckContextProps } from './utils/get_merged_indices_check_context_props'; +import { HistoricalResultsContext } from '../../data_quality_details/indices_details/pattern/contexts/historical_results_context'; +import { initialFetchHistoricalResultsReducerState } from '../../data_quality_details/indices_details/pattern/hooks/use_historical_results'; +import { + FetchHistoricalResultsReducerState, + UseHistoricalResultsReturnValue, +} from '../../data_quality_details/indices_details/pattern/hooks/use_historical_results/types'; interface TestExternalProvidersProps { children: React.ReactNode; @@ -171,3 +177,30 @@ const TestDataQualityProvidersComponent: React.FC TestDataQualityProvidersComponent.displayName = 'TestDataQualityProvidersComponent'; export const TestDataQualityProviders = React.memo(TestDataQualityProvidersComponent); + +export interface TestHistoricalResultsProviderProps { + children: React.ReactNode; + historicalResultsState?: FetchHistoricalResultsReducerState; + fetchHistoricalResults?: UseHistoricalResultsReturnValue['fetchHistoricalResults']; +} + +const TestHistoricalResultsProviderComponent: React.FC = ({ + children, + historicalResultsState = initialFetchHistoricalResultsReducerState, + fetchHistoricalResults = jest.fn(), +}) => { + return ( + + {children} + + ); +}; + +TestHistoricalResultsProviderComponent.displayName = 'TestHistoricalResultsProviderComponent'; + +export const TestHistoricalResultsProvider = React.memo(TestHistoricalResultsProviderComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_indices_check_context_props.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_indices_check_context_props.ts index 74817057ee377..05dd85e9d42b9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_indices_check_context_props.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_indices_check_context_props.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getCheckState } from '../../../stub/get_check_state'; +import { getCheckStateStub } from '../../../stub/get_check_state_stub'; import { UseIndicesCheckCheckState, UseIndicesCheckReturnValue, @@ -19,7 +19,7 @@ export const getMergedIndicesCheckContextProps = ( (acc, key) => { for (const indexName of patternIndexNames[key]) { acc[indexName] = { - ...getCheckState(indexName)[indexName], + ...getCheckStateStub(indexName)[indexName], }; } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/generate_historical_results_stub/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/generate_historical_results_stub/index.ts new file mode 100644 index 0000000000000..6155aeba962a2 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/generate_historical_results_stub/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHistoricalResult } from '../../mock/historical_results/mock_historical_results_response'; +import { HistoricalResult } from '../../types'; + +const dayInMs = 24 * 60 * 60 * 1000; + +export const generateHistoricalResultsStub = (indexName: string, amount = 1): HistoricalResult[] => + Array.from({ length: amount }, (_, i) => ({ + ...mockHistoricalResult, + indexName, + checkedAt: mockHistoricalResult.checkedAt + i * dayInMs, + markdownComments: [ + `### ${indexName}\n`, + `| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | ${indexName} | 618,675 (29.4%) | 1 | \`unmanaged\` | 81.2MB |\n\n`, + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + `\n\n#### Incompatible field values - ${indexName}\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | \`api\`, \`authentication\`, \`configuration\`, \`database\`, \`driver\`, \`email\`, \`file\`, \`host\`, \`iam\`, \`intrusion_detection\`, \`library\`, \`malware\`, \`network\`, \`package\`, \`process\`, \`registry\`, \`session\`, \`threat\`, \`vulnerability\`, \`web\` | \`siem\` (110616) |\n\n`, + ], + '@timestamp': mockHistoricalResult['@timestamp'] + i * dayInMs, + })); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state_stub/index.ts similarity index 98% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state_stub/index.ts index 1c56d25367e3a..16384c6ceb550 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state_stub/index.ts @@ -16,7 +16,7 @@ import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_un import { UnallowedValueSearchResult } from '../../types'; import { getMappingsProperties, getSortedPartitionedFieldMetadata } from '../../utils/metadata'; -export const getCheckState = ( +export const getCheckStateStub = ( indexName: string, indexCheckState?: Partial ) => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_historical_result_stub/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_historical_result_stub/index.ts new file mode 100644 index 0000000000000..0ac527f0fd5ae --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_historical_result_stub/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHistoricalResult } from '../../mock/historical_results/mock_historical_results_response'; +import type { LegacyHistoricalResult, NonLegacyHistoricalResult } from '../../types'; + +export const getHistoricalResultStub = (indexName: string): NonLegacyHistoricalResult => ({ + ...mockHistoricalResult, + indexName, + markdownComments: [ + `### ${indexName}\n`, + `| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | ${indexName} | 618,675 (29.4%) | 1 | \`unmanaged\` | 81.2MB |\n\n`, + '### **Incompatible fields** `1` **Same family** `3` **Custom fields** `64` **ECS compliant fields** `44` **All fields** `112`\n', + "#### 1 incompatible field\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + `\n\n#### Incompatible field values - ${indexName}\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | \`api\`, \`authentication\`, \`configuration\`, \`database\`, \`driver\`, \`email\`, \`file\`, \`host\`, \`iam\`, \`intrusion_detection\`, \`library\`, \`malware\`, \`network\`, \`package\`, \`process\`, \`registry\`, \`session\`, \`threat\`, \`vulnerability\`, \`web\` | \`siem\` (110616) |\n\n`, + ], +}); + +export const getLegacyHistoricalResultStub = (indexName: string): LegacyHistoricalResult => { + const NonLegacyHistoricalResult = getHistoricalResultStub(indexName); + + const { + incompatibleFieldMappingItems, + incompatibleFieldValueItems, + sameFamilyFieldItems, + ...legacyHistoricalResult + } = NonLegacyHistoricalResult; + + return legacyHistoricalResult; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts index 4b0e698d0afb2..93150b79138dc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts @@ -310,13 +310,6 @@ export const DOCS = i18n.translate('securitySolutionPackages.ecsDataQualityDashb defaultMessage: 'Docs', }); -export const INCOMPATIBLE_FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleFields', - { - defaultMessage: 'Incompatible fields', - } -); - export const INDICES = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indices', { defaultMessage: 'Indices', }); @@ -389,3 +382,125 @@ export const VIEW_INDEX_METADATA = i18n.translate( defaultMessage: 'view_index_metadata', } ); + +export const FIELD = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.field', { + defaultMessage: 'Field', +}); + +export const ECS_MAPPING_TYPE_EXPECTED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsMappingTypeExpected', + { + defaultMessage: 'ECS mapping type (expected)', + } +); + +export const INDEX_MAPPING_TYPE_ACTUAL = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexMappingTypeActual', + { + defaultMessage: 'Index mapping type (actual)', + } +); + +export const DOCUMENT_VALUES_ACTUAL = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.documentValuesActual', + { + defaultMessage: 'Document values (actual)', + } +); + +export const ECS_VALUES_EXPECTED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsValuesExpected', + { + defaultMessage: 'ECS values (expected)', + } +); + +export const INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE = (indexName: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldMappings', { + values: { indexName }, + defaultMessage: 'Incompatible field mappings - {indexName}', + }); + +export const INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE = (indexName: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldValues', { + values: { indexName }, + defaultMessage: 'Incompatible field values - {indexName}', + }); + +export const SAME_FAMILY_BADGE_LABEL = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel', + { + defaultMessage: 'same family', + } +); + +export const INCOMPATIBLE_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleFields', + { + defaultMessage: 'Incompatible fields', + } +); + +export const SAME_FAMILY = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.sameFamily', + { + defaultMessage: 'Same family', + } +); + +export const CUSTOM_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.customFields', + { + defaultMessage: 'Custom fields', + } +); + +export const ECS_COMPLIANT_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ecsCompliantFields', + { + defaultMessage: 'ECS compliant fields', + } +); + +export const ALL_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.allFields', + { + defaultMessage: 'All fields', + } +); + +export const INCOMPATIBLE_CALLOUT = (version: string) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.incompatibleCallout', { + values: { version }, + defaultMessage: + "Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version {version}.", + }); + +export const INCOMPATIBLE_CALLOUT_TITLE = (fieldCount: number) => + i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.incompatibleCalloutTitle', { + values: { fieldCount }, + defaultMessage: '{fieldCount} incompatible {fieldCount, plural, =1 {field} other {fields}}', + }); + +export const DETECTION_ENGINE_RULES_MAY_NOT_MATCH = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.detectionEngineRulesWontWorkMessage', + { + defaultMessage: + '❌ Detection engine rules referencing these fields may not match them correctly', + } +); + +export const PAGES_MAY_NOT_DISPLAY_EVENTS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.pagesMayNotDisplayEventsMessage', + { + defaultMessage: + '❌ Pages may not display some events or fields due to unexpected field mappings or values', + } +); + +export const MAPPINGS_THAT_CONFLICT_WITH_ECS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.mappingThatConflictWithEcsMessage', + { + defaultMessage: "❌ Mappings or field values that don't comply with ECS are not supported", + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts index 08c964d423101..2f96e5e4e1d2f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts @@ -66,6 +66,7 @@ export interface CustomFieldMetadata { isEcsCompliant: false; isInSameFamily: false; } + export interface EcsBasedFieldMetadata extends EcsFieldMetadata { hasEcsMetadata: true; indexFieldName: string; @@ -75,14 +76,33 @@ export interface EcsBasedFieldMetadata extends EcsFieldMetadata { isInSameFamily: boolean; } -export type EnrichedFieldMetadata = EcsBasedFieldMetadata | CustomFieldMetadata; +export interface IncompatibleFieldMetadata extends EcsBasedFieldMetadata { + isInSameFamily: false; + isEcsCompliant: false; +} + +export interface SameFamilyFieldMetadata extends EcsBasedFieldMetadata { + isEcsCompliant: false; + isInSameFamily: true; +} + +export interface EcsCompliantFieldMetadata extends EcsBasedFieldMetadata { + isEcsCompliant: true; + isInSameFamily: false; +} + +export type EnrichedFieldMetadata = + | EcsCompliantFieldMetadata + | CustomFieldMetadata + | IncompatibleFieldMetadata + | SameFamilyFieldMetadata; export interface PartitionedFieldMetadata { all: EnrichedFieldMetadata[]; custom: CustomFieldMetadata[]; - ecsCompliant: EcsBasedFieldMetadata[]; - incompatible: EcsBasedFieldMetadata[]; - sameFamily: EcsBasedFieldMetadata[]; + ecsCompliant: EcsCompliantFieldMetadata[]; + incompatible: IncompatibleFieldMetadata[]; + sameFamily: SameFamilyFieldMetadata[]; } export interface UnallowedValueRequestItem { @@ -287,3 +307,43 @@ export interface IndexSummaryTableItem { sizeInBytes: number | undefined; checkedAt: number | undefined; } + +export interface StorageResultBase { + batchId: string; + indexName: string; + indexPattern: string; + isCheckAll: boolean; + checkedAt: number; + docsCount: number; + totalFieldCount: number; + ecsFieldCount: number; + customFieldCount: number; + incompatibleFieldCount: number; + sameFamilyFieldCount: number; + sameFamilyFields: string[]; + unallowedMappingFields: string[]; + unallowedValueFields: string[]; + sizeInBytes: number; + ilmPhase?: IlmPhase; + markdownComments: string[]; + ecsVersion: string; + indexId: string; + error: string | null; +} + +export interface StorageResult extends StorageResultBase { + incompatibleFieldMappingItems: IncompatibleFieldMappingItem[]; + incompatibleFieldValueItems: IncompatibleFieldValueItem[]; + sameFamilyFieldItems: SameFamilyFieldItem[]; +} + +export interface HistoricalResultBase { + '@timestamp': number; + checkedBy: string; +} + +export interface LegacyHistoricalResult extends StorageResultBase, HistoricalResultBase {} + +export interface NonLegacyHistoricalResult extends StorageResult, HistoricalResultBase {} + +export type HistoricalResult = LegacyHistoricalResult | NonLegacyHistoricalResult; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.test.ts index a8bbbc0cd5c60..27acf3029f153 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.test.ts @@ -9,14 +9,51 @@ import numeral from '@elastic/numeral'; import { EMPTY_STAT } from '../constants'; import { + escapeNewlines, + escapePreserveNewlines, + getAllIncompatibleMarkdownComments, + getAllowedValues, getCodeFormattedValue, getHeaderSeparator, + getIncompatibleFieldsMarkdownComment, + getIncompatibleFieldsMarkdownTablesComment, + getIncompatibleMappings, + getIncompatibleMappingsMarkdownTableRows, + getIncompatibleValues, + getIncompatibleValuesMarkdownTableRows, + getIndexInvalidValues, + getMarkdownComment, + getMarkdownTable, getMarkdownTableHeader, getResultEmoji, getStatsRollupMarkdownComment, + getSummaryMarkdownComment, + getSummaryTableMarkdownComment, getSummaryTableMarkdownHeader, getSummaryTableMarkdownRow, + getTabCountsMarkdownComment, } from './markdown'; +import { mockPartitionedFieldMetadata } from '../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { mockAllowedValues } from '../mock/allowed_values/mock_allowed_values'; +import { UnallowedValueCount } from '../types'; +import { + eventCategory, + hostNameWithTextMapping, + mockIncompatibleMappings, + sourceIpWithTextMapping, +} from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { + DETECTION_ENGINE_RULES_MAY_NOT_MATCH, + ECS_MAPPING_TYPE_EXPECTED, + FIELD, + INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, + INDEX_MAPPING_TYPE_ACTUAL, + MAPPINGS_THAT_CONFLICT_WITH_ECS, + PAGES_MAY_NOT_DISPLAY_EVENTS, +} from '../translations'; +import { EcsVersion } from '@elastic/ecs'; + +const indexName = 'auditbeat-custom-index-1'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -120,7 +157,7 @@ describe('getSummaryTableMarkdownRow', () => { docsCount: 4, formatBytes, formatNumber, - incompatible: 3, + incompatibleFieldsCount: 3, ilmPhase: 'unmanaged', isILMAvailable: true, indexName: 'auditbeat-custom-index-1', @@ -136,7 +173,7 @@ describe('getSummaryTableMarkdownRow', () => { docsCount: 4, formatBytes, formatNumber, - incompatible: undefined, // <-- + incompatibleFieldsCount: undefined, // <-- ilmPhase: undefined, // <-- indexName: 'auditbeat-custom-index-1', isILMAvailable: true, @@ -152,7 +189,7 @@ describe('getSummaryTableMarkdownRow', () => { docsCount: 4, formatBytes, formatNumber, - incompatible: undefined, // <-- + incompatibleFieldsCount: undefined, // <-- ilmPhase: undefined, // <-- indexName: 'auditbeat-custom-index-1', isILMAvailable: false, @@ -168,7 +205,7 @@ describe('getSummaryTableMarkdownRow', () => { docsCount: 4, formatBytes, formatNumber, - incompatible: undefined, // <-- + incompatibleFieldsCount: undefined, // <-- ilmPhase: undefined, // <-- indexName: 'auditbeat-custom-index-1', isILMAvailable: false, @@ -177,6 +214,22 @@ describe('getSummaryTableMarkdownRow', () => { }) ).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- |\n'); }); + + test('it returns the expected row when patternDocsCount is undefined', () => { + expect( + getSummaryTableMarkdownRow({ + docsCount: 4, + formatBytes, + formatNumber, + incompatibleFieldsCount: undefined, // <-- + ilmPhase: undefined, // <-- + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + patternDocsCount: undefined, + sizeInBytes: undefined, + }) + ).toEqual('| -- | auditbeat-custom-index-1 | 4 | -- |\n'); + }); }); describe('getResultEmoji', () => { @@ -206,3 +259,591 @@ describe('getMarkdownTableHeader', () => { ); }); }); + +describe('getSummaryTableMarkdownComment', () => { + test('it returns the expected comment', () => { + expect( + getSummaryTableMarkdownComment({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: true, + incompatibleFieldsCount: 3, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual( + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n' + ); + }); + + test('it returns the expected comment when isILMAvailable is false', () => { + expect( + getSummaryTableMarkdownComment({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + incompatibleFieldsCount: 3, + patternDocsCount: 57410, + sizeInBytes: undefined, + }) + ).toEqual( + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n' + ); + }); + + test('it returns the expected comment when sizeInBytes is undefined', () => { + expect( + getSummaryTableMarkdownComment({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + incompatibleFieldsCount: 3, + patternDocsCount: 57410, + sizeInBytes: undefined, + }) + ).toEqual( + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n' + ); + }); +}); + +describe('escapeNewlines', () => { + test("it returns the content unmodified when there's nothing to escape", () => { + const content = "there's nothing to escape in this content"; + expect(escapeNewlines(content)).toEqual(content); + }); + + test('it replaces all newlines in the content with spaces', () => { + const content = '\nthere were newlines in the beginning, middle,\nand end\n'; + expect(escapeNewlines(content)).toEqual( + ' there were newlines in the beginning, middle, and end ' + ); + }); + + test('it escapes all column separators in the content with spaces', () => { + const content = '|there were column separators in the beginning, middle,|and end|'; + expect(escapeNewlines(content)).toEqual( + '\\|there were column separators in the beginning, middle,\\|and end\\|' + ); + }); + + test('it escapes content containing BOTH newlines and column separators', () => { + const content = + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; + expect(escapeNewlines(content)).toEqual( + '\\| there were newlines and column separators in the beginning, middle, \\|and end\\| ' + ); + }); +}); + +describe('escapePreserveNewlines', () => { + test('it returns undefined when `content` is undefined', () => { + expect(escapePreserveNewlines(undefined)).toBeUndefined(); + }); + + test("it returns the content unmodified when there's nothing to escape", () => { + const content = "there's (also) nothing to escape in this content"; + expect(escapePreserveNewlines(content)).toEqual(content); + }); + + test('it escapes all column separators in the content with spaces', () => { + const content = '|there were column separators in the beginning, middle,|and end|'; + expect(escapePreserveNewlines(content)).toEqual( + '\\|there were column separators in the beginning, middle,\\|and end\\|' + ); + }); + + test('it does NOT escape newlines in the content', () => { + const content = + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; + expect(escapePreserveNewlines(content)).toEqual( + '\\|\nthere were newlines and column separators in the beginning, middle,\n\\|and end\\|\n' + ); + }); +}); + +describe('getAllowedValues', () => { + test('it returns the expected placeholder when `allowedValues` is undefined', () => { + expect(getAllowedValues(undefined)).toEqual('`--`'); + }); + + test('it joins the `allowedValues` `name`s as a markdown-code-formatted, comma separated, string', () => { + expect(getAllowedValues(mockAllowedValues)).toEqual( + '`authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web`' + ); + }); +}); + +describe('getIndexInvalidValues', () => { + test('it returns the expected placeholder when `indexInvalidValues` is empty', () => { + expect(getIndexInvalidValues([])).toEqual('`--`'); + }); + + test('it returns markdown-code-formatted `fieldName`s, and their associated `count`s', () => { + const indexInvalidValues: UnallowedValueCount[] = [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ]; + + expect(getIndexInvalidValues(indexInvalidValues)).toEqual( + `\`an_invalid_category\` (2), \`theory\` (1)` + ); + }); +}); + +describe('getIncompatibleMappingsMarkdownTableRows', () => { + test('it returns the expected table rows', () => { + expect( + getIncompatibleMappingsMarkdownTableRows([hostNameWithTextMapping, sourceIpWithTextMapping]) + ).toEqual('| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |'); + }); +}); + +describe('getIncompatibleValuesMarkdownTableRows', () => { + test('it returns the expected table rows', () => { + expect( + getIncompatibleValuesMarkdownTableRows([ + { + ...eventCategory, + hasEcsMetadata: true, + indexInvalidValues: [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + isEcsCompliant: false, + }, + ]) + ).toEqual( + '| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |' + ); + }); +}); + +describe('getMarkdownComment', () => { + test('it returns the expected markdown comment', () => { + const suggestedAction = + '|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n|and end|\n'; + const title = + '|\nthere were newlines and column separators in this title beginning, middle,\n|and end|\n'; + + expect(getMarkdownComment({ suggestedAction, title })).toEqual( + '#### \\| there were newlines and column separators in this title beginning, middle, \\|and end\\| \n\n\\|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n\\|and end\\|\n' + ); + }); +}); + +describe('getMarkdownTable', () => { + test('it returns the expected table contents', () => { + expect( + getMarkdownTable({ + enrichedFieldMetadata: mockIncompatibleMappings, + getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + ).toEqual( + '#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n' + ); + }); + + test('it returns an empty string when `enrichedFieldMetadata` is empty', () => { + expect( + getMarkdownTable({ + enrichedFieldMetadata: [], // <-- empty + getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + ).toEqual(''); + }); +}); + +describe('getIncompatibleFieldsMarkdownComment', () => { + test('it returns the expected counts and ECS version', () => { + expect(getIncompatibleFieldsMarkdownComment(11)).toEqual(`#### 11 incompatible fields + +Fields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}. + +${DETECTION_ENGINE_RULES_MAY_NOT_MATCH} +${PAGES_MAY_NOT_DISPLAY_EVENTS} +${MAPPINGS_THAT_CONFLICT_WITH_ECS} +`); + }); +}); + +describe('getIncompatibleMappings', () => { + test('it (only) returns the mappings where type !== indexFieldType', () => { + expect(getIncompatibleMappings(mockPartitionedFieldMetadata.incompatible)).toEqual([ + { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + hasEcsMetadata: true, + ignore_above: 1024, + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + }, + { + dashed_name: 'source-ip', + description: 'IP address of the source (IPv4 or IPv6).', + flat_name: 'source.ip', + hasEcsMetadata: true, + indexFieldName: 'source.ip', + indexFieldType: 'text', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + level: 'core', + name: 'ip', + normalize: [], + short: 'IP address of the source.', + type: 'ip', + }, + ]); + }); +}); + +describe('getIncompatibleValues', () => { + test('it (only) returns the mappings with indexInvalidValues', () => { + expect(getIncompatibleValues(mockPartitionedFieldMetadata.incompatible)).toEqual([ + { + allowed_values: [ + { + description: + 'Events in this category are related to the challenge and response process in which credentials are supplied and verified to allow the creation of a session. Common sources for these logs are Windows event logs and ssh logs. Visualize and analyze events in this category to look for failed logins, and other authentication-related activity.', + expected_event_types: ['start', 'end', 'info'], + name: 'authentication', + }, + { + description: + 'Events in the configuration category have to deal with creating, modifying, or deleting the settings or parameters of an application, process, or system.\nExample sources include security policy change logs, configuration auditing logging, and system integrity monitoring.', + expected_event_types: ['access', 'change', 'creation', 'deletion', 'info'], + name: 'configuration', + }, + { + description: + 'The database category denotes events and metrics relating to a data storage and retrieval system. Note that use of this category is not limited to relational database systems. Examples include event logs from MS SQL, MySQL, Elasticsearch, MongoDB, etc. Use this category to visualize and analyze database activity such as accesses and changes.', + expected_event_types: ['access', 'change', 'info', 'error'], + name: 'database', + }, + { + description: + 'Events in the driver category have to do with operating system device drivers and similar software entities such as Windows drivers, kernel extensions, kernel modules, etc.\nUse events and metrics in this category to visualize and analyze driver-related activity and status on hosts.', + expected_event_types: ['change', 'end', 'info', 'start'], + name: 'driver', + }, + { + description: + 'This category is used for events relating to email messages, email attachments, and email network or protocol activity.\nEmails events can be produced by email security gateways, mail transfer agents, email cloud service providers, or mail server monitoring applications.', + expected_event_types: ['info'], + name: 'email', + }, + { + description: + 'Relating to a set of information that has been created on, or has existed on a filesystem. Use this category of events to visualize and analyze the creation, access, and deletions of files. Events in this category can come from both host-based and network-based sources. An example source of a network-based detection of a file transfer would be the Zeek file.log.', + expected_event_types: ['change', 'creation', 'deletion', 'info'], + name: 'file', + }, + { + description: + 'Use this category to visualize and analyze information such as host inventory or host lifecycle events.\nMost of the events in this category can usually be observed from the outside, such as from a hypervisor or a control plane\'s point of view. Some can also be seen from within, such as "start" or "end".\nNote that this category is for information about hosts themselves; it is not meant to capture activity "happening on a host".', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'host', + }, + { + description: + 'Identity and access management (IAM) events relating to users, groups, and administration. Use this category to visualize and analyze IAM-related logs and data from active directory, LDAP, Okta, Duo, and other IAM systems.', + expected_event_types: [ + 'admin', + 'change', + 'creation', + 'deletion', + 'group', + 'info', + 'user', + ], + name: 'iam', + }, + { + description: + 'Relating to intrusion detections from IDS/IPS systems and functions, both network and host-based. Use this category to visualize and analyze intrusion detection alerts from systems such as Snort, Suricata, and Palo Alto threat detections.', + expected_event_types: ['allowed', 'denied', 'info'], + name: 'intrusion_detection', + }, + { + description: + 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.', + expected_event_types: ['info'], + name: 'malware', + }, + { + description: + 'Relating to all network activity, including network connection lifecycle, network traffic, and essentially any event that includes an IP address. Many events containing decoded network protocol transactions fit into this category. Use events in this category to visualize or analyze counts of network ports, protocols, addresses, geolocation information, etc.', + expected_event_types: [ + 'access', + 'allowed', + 'connection', + 'denied', + 'end', + 'info', + 'protocol', + 'start', + ], + name: 'network', + }, + { + description: + 'Relating to software packages installed on hosts. Use this category to visualize and analyze inventory of software installed on various hosts, or to determine host vulnerability in the absence of vulnerability scan data.', + expected_event_types: ['access', 'change', 'deletion', 'info', 'installation', 'start'], + name: 'package', + }, + { + description: + 'Use this category of events to visualize and analyze process-specific information such as lifecycle events or process ancestry.', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'process', + }, + { + description: + 'Having to do with settings and assets stored in the Windows registry. Use this category to visualize and analyze activity such as registry access and modifications.', + expected_event_types: ['access', 'change', 'creation', 'deletion'], + name: 'registry', + }, + { + description: + 'The session category is applied to events and metrics regarding logical persistent connections to hosts and services. Use this category to visualize and analyze interactive or automated persistent connections between assets. Data for this category may come from Windows Event logs, SSH logs, or stateless sessions such as HTTP cookie-based sessions, etc.', + expected_event_types: ['start', 'end', 'info'], + name: 'session', + }, + { + description: + "Use this category to visualize and analyze events describing threat actors' targets, motives, or behaviors.", + expected_event_types: ['indicator'], + name: 'threat', + }, + { + description: + 'Relating to vulnerability scan results. Use this category to analyze vulnerabilities detected by Tenable, Qualys, internal scanners, and other vulnerability management sources.', + expected_event_types: ['info'], + name: 'vulnerability', + }, + { + description: + 'Relating to web server access. Use this category to create a dashboard of web server/proxy activity from apache, IIS, nginx web servers, etc. Note: events from network observers such as Zeek http log may also be included in this category.', + expected_event_types: ['access', 'error', 'info'], + name: 'web', + }, + ], + dashed_name: 'event-category', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + flat_name: 'event.category', + ignore_above: 1024, + level: 'core', + name: 'category', + normalize: ['array'], + short: 'Event category. The second categorization field in the hierarchy.', + type: 'keyword', + indexFieldName: 'event.category', + indexFieldType: 'keyword', + indexInvalidValues: [ + { count: 2, fieldName: 'an_invalid_category' }, + { count: 1, fieldName: 'theory' }, + ], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + ]); + }); +}); + +describe('getIncompatibleFieldsMarkdownTablesComment', () => { + test('it returns the expected comment when the index has `incompatibleMappings` and `incompatibleValues`', () => { + expect( + getIncompatibleFieldsMarkdownTablesComment({ + incompatibleMappingsFields: [ + mockPartitionedFieldMetadata.incompatible[1], + mockPartitionedFieldMetadata.incompatible[2], + ], + incompatibleValuesFields: [mockPartitionedFieldMetadata.incompatible[0]], + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual( + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n' + ); + }); + + test('it returns the expected comment when the index does NOT have `incompatibleMappings` and `incompatibleValues`', () => { + expect( + getIncompatibleFieldsMarkdownTablesComment({ + incompatibleMappingsFields: [], // <-- no `incompatibleMappings` + incompatibleValuesFields: [], // <-- no `incompatibleValues` + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual('\n\n\n'); + }); +}); + +describe('getAllIncompatibleMarkdownComments', () => { + test('it returns the expected collection of comments', () => { + expect( + getAllIncompatibleMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + isILMAvailable: true, + indexName: 'auditbeat-custom-index-1', + incompatibleMappingsFields: [ + mockPartitionedFieldMetadata.incompatible[1], + mockPartitionedFieldMetadata.incompatible[2], + ], + incompatibleValuesFields: [mockPartitionedFieldMetadata.incompatible[0]], + sameFamilyFieldsCount: 0, + customFieldsCount: 4, + ecsCompliantFieldsCount: 2, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n${DETECTION_ENGINE_RULES_MAY_NOT_MATCH}\n${PAGES_MAY_NOT_DISPLAY_EVENTS}\n${MAPPINGS_THAT_CONFLICT_WITH_ECS}\n`, + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ]); + }); + + test('it returns the expected comment when `incompatible` is empty', () => { + expect( + getAllIncompatibleMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: true, + incompatibleMappingsFields: [], + incompatibleValuesFields: [], + sameFamilyFieldsCount: 0, + customFieldsCount: 4, + ecsCompliantFieldsCount: 2, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + '\n\n\n', + ]); + }); + + test('it returns the expected comment when `isILMAvailable` is false', () => { + expect( + getAllIncompatibleMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + incompatibleMappingsFields: [], + incompatibleValuesFields: [], + sameFamilyFieldsCount: 0, + customFieldsCount: 4, + ecsCompliantFieldsCount: 2, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: undefined, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 |\n\n', + '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + '\n\n\n', + ]); + }); + + test('it returns the expected comment when `sizeInBytes` is not an integer', () => { + expect( + getAllIncompatibleMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + isILMAvailable: false, + incompatibleMappingsFields: [], + incompatibleValuesFields: [], + sameFamilyFieldsCount: 0, + customFieldsCount: 4, + ecsCompliantFieldsCount: 2, + allFieldsCount: 9, + patternDocsCount: 57410, + sizeInBytes: undefined, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 |\n\n', + '### **Incompatible fields** `0` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + '\n\n\n', + ]); + }); +}); + +describe('getSummaryMarkdownComment', () => { + test('it returns the expected markdown comment', () => { + expect(getSummaryMarkdownComment(indexName)).toEqual('### auditbeat-custom-index-1\n'); + }); +}); + +describe('getTabCountsMarkdownComment', () => { + test('it returns a comment with the expected counts', () => { + expect( + getTabCountsMarkdownComment({ + incompatibleFieldsCount: 3, + sameFamilyFieldsCount: 0, + customFieldsCount: 4, + ecsCompliantFieldsCount: 2, + allFieldsCount: 9, + }) + ).toBe( + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n' + ); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.ts index e8118df379c82..2002e89f3224f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/markdown.ts @@ -5,23 +5,46 @@ * 2.0. */ +import { EcsVersion } from '@elastic/ecs'; import { EMPTY_PLACEHOLDER, EMPTY_STAT } from '../constants'; import { + ALL_FIELDS, + CUSTOM_FIELDS, + DETECTION_ENGINE_RULES_MAY_NOT_MATCH, DOCS, + DOCUMENT_VALUES_ACTUAL, + ECS_COMPLIANT_FIELDS, + ECS_MAPPING_TYPE_EXPECTED, + ECS_VALUES_EXPECTED, + FIELD, ILM_PHASE, ILM_PHASE_CAPITALIZED, + INCOMPATIBLE_CALLOUT, + INCOMPATIBLE_CALLOUT_TITLE, INCOMPATIBLE_FIELDS, + INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, + INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE, INDEX, + INDEX_MAPPING_TYPE_ACTUAL, INDICES, INDICES_CHECKED, + MAPPINGS_THAT_CONFLICT_WITH_ECS, + PAGES_MAY_NOT_DISPLAY_EVENTS, RESULT, + SAME_FAMILY, SIZE, } from '../translations'; -import { IlmPhase } from '../types'; +import { + AllowedValue, + EnrichedFieldMetadata, + IlmPhase, + IncompatibleFieldMetadata, + UnallowedValueCount, +} from '../types'; import { getDocsCountPercent } from './stats'; -export const escapeNewlines = (content: string | undefined): string | undefined => - content != null ? content.replaceAll('\n', ' ').replaceAll('|', '\\|') : content; +export const escapeNewlines = (content: string): string => + content.replaceAll('\n', ' ').replaceAll('|', '\\|'); export const getCodeFormattedValue = (value: string | undefined) => `\`${escapeNewlines(value ?? EMPTY_PLACEHOLDER)}\``; @@ -76,11 +99,11 @@ export const getSummaryTableMarkdownHeader = (includeDocSize: boolean): string = DOCS )}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|`; -export const getResultEmoji = (incompatible: number | undefined): string => { - if (incompatible == null) { +export const getResultEmoji = (incompatibleCount: number | undefined): string => { + if (incompatibleCount == null) { return EMPTY_PLACEHOLDER; } else { - return incompatible === 0 ? '✅' : '❌'; + return incompatibleCount === 0 ? '✅' : '❌'; } }; @@ -89,7 +112,7 @@ export const getSummaryTableMarkdownRow = ({ formatBytes, formatNumber, ilmPhase, - incompatible, + incompatibleFieldsCount, indexName, isILMAvailable, patternDocsCount, @@ -99,30 +122,279 @@ export const getSummaryTableMarkdownRow = ({ formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; - incompatible: number | undefined; + incompatibleFieldsCount: number | undefined; indexName: string; isILMAvailable: boolean; - patternDocsCount: number; + patternDocsCount?: number; sizeInBytes: number | undefined; -}): string => - isILMAvailable && Number.isInteger(sizeInBytes) - ? `| ${getResultEmoji(incompatible)} | ${escapeNewlines(indexName)} | ${formatNumber( - docsCount - )} (${getDocsCountPercent({ - docsCount, - patternDocsCount, - })}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${ - ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER - } | ${formatBytes(sizeInBytes)} | -` - : `| ${getResultEmoji(incompatible)} | ${escapeNewlines(indexName)} | ${formatNumber( - docsCount - )} (${getDocsCountPercent({ - docsCount, - patternDocsCount, - })}) | ${incompatible ?? EMPTY_PLACEHOLDER} | +}): string => { + const emojiColumn = getResultEmoji(incompatibleFieldsCount); + const indexNameColumn = escapeNewlines(indexName); + const docsCountColumn = formatNumber(docsCount); + const incompatibleFieldsCountColumn = formatNumber(incompatibleFieldsCount); + const docsCountPercentColumn = patternDocsCount + ? `(${getDocsCountPercent({ docsCount, patternDocsCount })}) ` + : ''; + const baseColumns = `${emojiColumn} | ${indexNameColumn} | ${docsCountColumn} ${docsCountPercentColumn}| ${incompatibleFieldsCountColumn}`; + + if (isILMAvailable && Number.isInteger(sizeInBytes)) { + const ilmPhaseColumn = ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER; + const sizeColumn = formatBytes(sizeInBytes); + return `| ${baseColumns} | ${ilmPhaseColumn} | ${sizeColumn} | `; + } + + return `| ${baseColumns} | +`; +}; export const getMarkdownTableHeader = (headerNames: string[]) => ` | ${headerNames.map((name) => `${escapeNewlines(name)} | `).join('')} |${headerNames.map((name) => `${getHeaderSeparator(name)}|`).join('')}`; + +export const getSummaryTableMarkdownComment = ({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleFieldsCount, + patternDocsCount, + sizeInBytes, +}: { + docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + ilmPhase: IlmPhase | undefined; + indexName: string; + isILMAvailable: boolean; + incompatibleFieldsCount: number; + patternDocsCount?: number; + sizeInBytes: number | undefined; +}): string => + `${getSummaryTableMarkdownHeader(isILMAvailable)} +${getSummaryTableMarkdownRow({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleFieldsCount, + patternDocsCount, + sizeInBytes, +})} +`; + +export const getSummaryMarkdownComment = (indexName: string) => + `### ${escapeNewlines(indexName)} +`; + +export const getTabCountsMarkdownComment = (tabCounts: { + allFieldsCount: number; + customFieldsCount: number; + ecsCompliantFieldsCount: number; + incompatibleFieldsCount: number; + sameFamilyFieldsCount: number; +}): string => + `### **${INCOMPATIBLE_FIELDS}** ${getCodeFormattedValue( + `${tabCounts.incompatibleFieldsCount}` + )} **${SAME_FAMILY}** ${getCodeFormattedValue( + `${tabCounts.sameFamilyFieldsCount}` + )} **${CUSTOM_FIELDS}** ${getCodeFormattedValue( + `${tabCounts.customFieldsCount}` + )} **${ECS_COMPLIANT_FIELDS}** ${getCodeFormattedValue( + `${tabCounts.ecsCompliantFieldsCount}` + )} **${ALL_FIELDS}** ${getCodeFormattedValue(`${tabCounts.allFieldsCount}`)} +`; + +export const getIncompatibleMappings = ( + incompatibleFieldMetadata: IncompatibleFieldMetadata[] +): IncompatibleFieldMetadata[] => + incompatibleFieldMetadata.filter((x) => x.type !== x.indexFieldType); + +export const getIncompatibleValues = ( + incompatibleFieldMetadata: IncompatibleFieldMetadata[] +): IncompatibleFieldMetadata[] => + incompatibleFieldMetadata.filter((x) => x.indexInvalidValues.length > 0); + +export const getIncompatibleFieldsMarkdownComment = (incompatible: number): string => + getMarkdownComment({ + suggestedAction: `${INCOMPATIBLE_CALLOUT(EcsVersion)} + +${DETECTION_ENGINE_RULES_MAY_NOT_MATCH} +${PAGES_MAY_NOT_DISPLAY_EVENTS} +${MAPPINGS_THAT_CONFLICT_WITH_ECS} +`, + title: INCOMPATIBLE_CALLOUT_TITLE(incompatible), + }); + +export const escapePreserveNewlines = (content: string | undefined): string | undefined => + content != null ? content.replaceAll('|', '\\|') : content; + +export const getIndexInvalidValues = (indexInvalidValues: UnallowedValueCount[]): string => + indexInvalidValues.length === 0 + ? getCodeFormattedValue(undefined) + : indexInvalidValues + .map( + ({ fieldName, count }) => `${getCodeFormattedValue(escapeNewlines(fieldName))} (${count})` + ) + .join(', '); // newlines are instead joined with spaces + +export const getAllowedValues = (allowedValues: AllowedValue[] | undefined): string => + allowedValues == null + ? getCodeFormattedValue(undefined) + : allowedValues.map((x) => getCodeFormattedValue(x.name)).join(', '); + +export const getIncompatibleValuesMarkdownTableRows = ( + incompatibleValuesFields: IncompatibleFieldMetadata[] +): string => + incompatibleValuesFields + .map( + (x) => + `| ${escapeNewlines(x.indexFieldName)} | ${getAllowedValues( + x.allowed_values + )} | ${getIndexInvalidValues(x.indexInvalidValues)} |` + ) + .join('\n'); + +export const getMarkdownComment = ({ + suggestedAction, + title, +}: { + suggestedAction: string; + title: string; +}): string => + `#### ${escapeNewlines(title)} + +${escapePreserveNewlines(suggestedAction)}`; + +export const getIncompatibleMappingsMarkdownTableRows = ( + incompatibleMappingsFields: IncompatibleFieldMetadata[] +): string => + incompatibleMappingsFields + .map( + (x) => + `| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue( + x.type + )} | ${getCodeFormattedValue(x.indexFieldType)} |` + ) + .join('\n'); + +export const getMarkdownTable = ({ + enrichedFieldMetadata, + getMarkdownTableRows, + headerNames, + title, +}: { + enrichedFieldMetadata: T; + getMarkdownTableRows: (enrichedFieldMetadata: T) => string; + headerNames: string[]; + title: string; +}): string => + enrichedFieldMetadata.length > 0 + ? `#### ${escapeNewlines(title)} + +${getMarkdownTableHeader(headerNames)} +${getMarkdownTableRows(enrichedFieldMetadata)} +` + : ''; + +export const getIncompatibleFieldsMarkdownTablesComment = ({ + incompatibleMappingsFields, + incompatibleValuesFields, + indexName, +}: { + incompatibleMappingsFields: IncompatibleFieldMetadata[]; + incompatibleValuesFields: IncompatibleFieldMetadata[]; + indexName: string; +}): string => ` +${ + incompatibleMappingsFields.length > 0 + ? getMarkdownTable({ + enrichedFieldMetadata: incompatibleMappingsFields, + getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + : '' +} +${ + incompatibleValuesFields.length > 0 + ? getMarkdownTable({ + enrichedFieldMetadata: incompatibleValuesFields, + getMarkdownTableRows: getIncompatibleValuesMarkdownTableRows, + headerNames: [FIELD, ECS_VALUES_EXPECTED, DOCUMENT_VALUES_ACTUAL], + title: INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE(indexName), + }) + : '' +} +`; + +export const getAllIncompatibleMarkdownComments = ({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleMappingsFields, + incompatibleValuesFields, + sameFamilyFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + allFieldsCount, + patternDocsCount, + sizeInBytes, +}: { + docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + ilmPhase: IlmPhase | undefined; + indexName: string; + isILMAvailable: boolean; + incompatibleMappingsFields: IncompatibleFieldMetadata[]; + incompatibleValuesFields: IncompatibleFieldMetadata[]; + sameFamilyFieldsCount: number; + customFieldsCount: number; + ecsCompliantFieldsCount: number; + allFieldsCount: number; + patternDocsCount?: number; + sizeInBytes: number | undefined; +}): string[] => { + const incompatibleFieldsCount = + incompatibleMappingsFields.length + incompatibleValuesFields.length; + const incompatibleFieldsMarkdownComment = + incompatibleFieldsCount > 0 + ? getIncompatibleFieldsMarkdownComment(incompatibleFieldsCount) + : ''; + + return [ + getSummaryMarkdownComment(indexName), + getSummaryTableMarkdownComment({ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + isILMAvailable, + incompatibleFieldsCount, + patternDocsCount, + sizeInBytes, + }), + getTabCountsMarkdownComment({ + allFieldsCount, + customFieldsCount, + ecsCompliantFieldsCount, + incompatibleFieldsCount, + sameFamilyFieldsCount, + }), + incompatibleFieldsMarkdownComment, + getIncompatibleFieldsMarkdownTablesComment({ + incompatibleMappingsFields, + incompatibleValuesFields, + indexName, + }), + ].filter((x) => x !== ''); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts index b85f4a7516dbb..0158a1d7820ef 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts @@ -17,6 +17,7 @@ import { getPartitionedFieldMetadata, getSortedPartitionedFieldMetadata, isMappingCompatible, + isTypeInSameFamily, } from './metadata'; import { EcsFlatTyped } from '../constants'; import { @@ -635,3 +636,35 @@ describe('getMappingsProperties', () => { ).toBeNull(); }); }); + +describe('isTypeInSameFamily', () => { + test('it returns false when ecsExpectedType is undefined', () => { + expect(isTypeInSameFamily({ ecsExpectedType: undefined, type: 'keyword' })).toBe(false); + }); + + const expectedFamilyMembers: { + [key: string]: string[]; + } = { + constant_keyword: ['keyword', 'wildcard'], // `keyword` and `wildcard` in the same family as `constant_keyword` + keyword: ['constant_keyword', 'wildcard'], + match_only_text: ['text'], + text: ['match_only_text'], + wildcard: ['keyword', 'constant_keyword'], + }; + + const ecsExpectedTypes = Object.keys(expectedFamilyMembers); + + ecsExpectedTypes.forEach((ecsExpectedType) => { + const otherMembersOfSameFamily = expectedFamilyMembers[ecsExpectedType]; + + otherMembersOfSameFamily.forEach((type) => + test(`it returns true for ecsExpectedType '${ecsExpectedType}' when given '${type}', a type in the same family`, () => { + expect(isTypeInSameFamily({ ecsExpectedType, type })).toBe(true); + }) + ); + + test(`it returns false for ecsExpectedType '${ecsExpectedType}' when given 'date', a type NOT in the same family`, () => { + expect(isTypeInSameFamily({ ecsExpectedType, type: 'date' })).toBe(false); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts index c2fb750e4f0a8..302b7d25bbf2b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts @@ -13,31 +13,52 @@ import { has, sortBy } from 'lodash/fp'; import { EMPTY_METADATA, EcsFlatTyped } from '../constants'; import { - EcsBasedFieldMetadata, + CustomFieldMetadata, + EcsCompliantFieldMetadata, EnrichedFieldMetadata, + IncompatibleFieldMetadata, PartitionedFieldMetadata, + SameFamilyFieldMetadata, UnallowedValueCount, } from '../types'; -import { getIsInSameFamily } from '../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family'; + +export const isEcsCompliantFieldMetadata = ( + x: EnrichedFieldMetadata +): x is EcsCompliantFieldMetadata => x.hasEcsMetadata && x.isEcsCompliant; + +export const isSameFamilyFieldMetadata = (x: EnrichedFieldMetadata): x is SameFamilyFieldMetadata => + x.hasEcsMetadata && !x.isEcsCompliant && x.isInSameFamily; + +export const isIncompatibleFieldMetadata = ( + x: EnrichedFieldMetadata +): x is IncompatibleFieldMetadata => x.hasEcsMetadata && !x.isEcsCompliant && !x.isInSameFamily; + +export const isCustomFieldMetadata = (x: EnrichedFieldMetadata): x is CustomFieldMetadata => + !x.hasEcsMetadata; export const getPartitionedFieldMetadata = ( enrichedFieldMetadata: EnrichedFieldMetadata[] ): PartitionedFieldMetadata => enrichedFieldMetadata.reduce( - (acc, x) => ({ - all: [...acc.all, x], - ecsCompliant: x.isEcsCompliant ? [...acc.ecsCompliant, x] : acc.ecsCompliant, - custom: !x.hasEcsMetadata ? [...acc.custom, x] : acc.custom, - incompatible: - x.hasEcsMetadata && !x.isEcsCompliant && !x.isInSameFamily - ? [...acc.incompatible, x] - : acc.incompatible, - sameFamily: x.isInSameFamily ? [...acc.sameFamily, x] : acc.sameFamily, - }), + (acc, field) => { + acc.all.push(field); + + if (isCustomFieldMetadata(field)) { + acc.custom.push(field); + } else if (isEcsCompliantFieldMetadata(field)) { + acc.ecsCompliant.push(field); + } else if (isSameFamilyFieldMetadata(field)) { + acc.sameFamily.push(field); + } else if (isIncompatibleFieldMetadata(field)) { + acc.incompatible.push(field); + } + + return acc; + }, { all: [], - ecsCompliant: [], custom: [], + ecsCompliant: [], incompatible: [], sameFamily: [], } @@ -109,6 +130,39 @@ export function getFieldTypes(mappingsProperties: Record): Fiel return result; } +/** + * Per https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html#_core_datatypes + * + * ``` + * Field types are grouped by _family_. Types in the same family have exactly + * the same search behavior but may have different space usage or + * performance characteristics. + * + * Currently, there are two type families, `keyword` and `text`. Other type + * families have only a single field type. For example, the `boolean` type + * family consists of one field type: `boolean`. + * ``` + */ +export const fieldTypeFamilies: Record> = { + keyword: new Set(['keyword', 'constant_keyword', 'wildcard']), + text: new Set(['text', 'match_only_text']), +}; + +export const isTypeInSameFamily = ({ + ecsExpectedType, + type, +}: { + ecsExpectedType: string | undefined; + type: string; +}): boolean => { + if (ecsExpectedType == null) { + return false; + } + + const allFamilies = Object.values(fieldTypeFamilies); + return allFamilies.some((family) => family.has(ecsExpectedType) && family.has(type)); +}; + export const isMappingCompatible = ({ ecsExpectedType, type, @@ -129,35 +183,65 @@ export const getEnrichedFieldMetadata = ({ const { field, type } = fieldMetadata; const indexInvalidValues = unallowedValues[field] ?? []; - if (has(fieldMetadata.field, ecsMetadata)) { + // Check if the field is ECS-based + if (has(field, ecsMetadata)) { const ecsExpectedType = ecsMetadata[field].type; + const isEcsCompliant = isMappingCompatible({ ecsExpectedType, type }) && indexInvalidValues.length === 0; const isInSameFamily = !isMappingCompatible({ ecsExpectedType, type }) && indexInvalidValues.length === 0 && - getIsInSameFamily({ ecsExpectedType, type }); + isTypeInSameFamily({ ecsExpectedType, type }); + + if (isEcsCompliant) { + return { + ...ecsMetadata[field], + indexFieldName: field, + indexFieldType: type, + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }; + } + // mutually exclusive with ECS compliant + // because of mappings compatibility check + if (isInSameFamily) { + return { + ...ecsMetadata[field], + indexFieldName: field, + indexFieldType: type, + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }; + } + + // incompatible field (not compliant and not in the same family) return { ...ecsMetadata[field], indexFieldName: field, indexFieldType: type, indexInvalidValues, hasEcsMetadata: true, - isEcsCompliant, - isInSameFamily, - }; - } else { - return { - indexFieldName: field, - indexFieldType: type, - indexInvalidValues: [], - hasEcsMetadata: false, isEcsCompliant: false, - isInSameFamily: false, // custom fields are never in the same family + isInSameFamily: false, }; } + + // custom field + return { + indexFieldName: field, + indexFieldType: type, + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }; }; export const getSortedPartitionedFieldMetadata = ({ @@ -203,7 +287,7 @@ export const getSortedPartitionedFieldMetadata = ({ return partitionedFieldMetadata; }; -export const getMissingTimestampFieldMetadata = (): EcsBasedFieldMetadata => ({ +export const getMissingTimestampFieldMetadata = (): IncompatibleFieldMetadata => ({ ...EcsFlatTyped['@timestamp'], hasEcsMetadata: true, indexFieldName: '@timestamp', diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.test.ts index fd48ba39073fe..1eae92242c842 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.test.ts @@ -10,8 +10,9 @@ import { auditbeatWithAllResults, } from '../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatWithSomeErrors } from '../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { mockStatsPacketbeatIndex } from '../mock/stats/mock_stats_auditbeat_index'; -import { mockStatsAuditbeatIndex } from '../mock/stats/mock_stats_packetbeat_index'; +import { mockStatsAuditbeatIndex } from '../mock/stats/mock_stats_auditbeat_index'; +import { mockStatsPacketbeatIndex } from '../mock/stats/mock_stats_packetbeat_index'; + import { DataQualityCheckResult } from '../types'; import { getDocsCount, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/setup_tests.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/setup_tests.ts index 72e0edd0d07f7..55be1202c0fa0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/setup_tests.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/setup_tests.ts @@ -7,3 +7,13 @@ // eslint-disable-next-line import/no-extraneous-dependencies import '@testing-library/jest-dom'; + +// context: +// https://github.com/elastic/eui/issues/4408#issuecomment-754125867 +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +// https://github.com/jsdom/jsdom/issues/1695 +window.HTMLElement.prototype.scrollIntoView = jest.fn(); diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts index b242b6f80b1b2..8ccb3fbc3f984 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; +import { StringToNonNegativeNumber } from './utils/string_to_non_negative_number'; const ResultDocumentInterface = t.interface({ batchId: t.string, @@ -74,7 +75,7 @@ export const GetIndexResultsParams = t.type({ export const GetIndexResultsQuery = t.partial({ size: StringToPositiveNumber, - from: StringToPositiveNumber, + from: StringToNonNegativeNumber, startDate: t.string, endDate: t.string, outcome: t.union([t.literal('pass'), t.literal('fail')]), diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/utils/string_to_non_negative_number.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/utils/string_to_non_negative_number.ts new file mode 100644 index 0000000000000..2c2b8ca8375a9 --- /dev/null +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/utils/string_to_non_negative_number.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; +import type { Either } from 'fp-ts/lib/Either'; + +export type StringToNonNegativeNumber = t.Type; + +/** + * Types the StringToNonNegativeNumber as: + * - If a string this converts the string into a number + * - Ensures it is a number (and not NaN) + * - Ensures it is non-negative (>= 0) number + */ + +export const StringToNonNegativeNumber: StringToNonNegativeNumber = new t.Type< + number, + string, + unknown +>( + 'StringToNonNegativeNumber', + t.number.is, + (input, context): Either => { + return either.chain( + t.string.validate(input, context), + (numberAsString): Either => { + const stringAsNumber = +numberAsString; + if (numberAsString.trim().length === 0 || isNaN(stringAsNumber) || stringAsNumber < 0) { + return t.failure(input, context); + } else { + return t.success(stringAsNumber); + } + } + ); + }, + String +); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3db116375ecbe..26ef42752ae79 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6949,15 +6949,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.checkingLabel": "Vérification de {index}", "securitySolutionPackages.ecsDataQualityDashboard.coldDescription": "L'index n'est plus mis à jour et il est interrogé peu fréquemment. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient plus lentes.", "securitySolutionPackages.ecsDataQualityDashboard.coldPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"cold\". Les index \"cold\" ne sont plus mis à jour et ne sont pas interrogés fréquemment. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient plus lentes.", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.documentValuesActualColumn": "Valeurs du document (réelles)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsDescriptionColumn": "Description ECS", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeColumn": "Type de mapping ECS", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeExpectedColumn": "Type de mapping ECS (attendu)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesColumn": "Valeurs ECS", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesExpectedColumn": "Valeurs ECS (attendues)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.fieldColumn": "Champ", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeActualColumn": "Type de mapping d'index (réel)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeColumn": "Type de mapping d'index", "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder": "Rechercher dans les champs", "securitySolutionPackages.ecsDataQualityDashboard.copyToClipboardButton": "Copier dans le presse-papiers", "securitySolutionPackages.ecsDataQualityDashboard.createADataQualityCaseForIndexHeaderText": "Créer un cas de qualité des données pour l'index {indexName}", @@ -7007,8 +6998,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "Sélectionner une ou plusieurs phases ILM", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "non géré", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "warm", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "Mappings de champ incompatibles – {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle": "Valeurs de champ incompatibles – {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "La qualité des données sera vérifiée pour les index comprenant ces phases de gestion du cycle de vie des index (ILM, Index Lifecycle Management)", "securitySolutionPackages.ecsDataQualityDashboard.indexNameLabel": "Nom de l'index", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton": "Ajouter au nouveau cas", @@ -7016,44 +7005,31 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyContent": "Cet index ne contient aucun mapping", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyTitle": "Aucun mapping", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutTitle": "L'ensemble {fieldCount} {fieldCount, plural, =1 {du mapping de champs} other {des mappings de champs}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allFieldsLabel": "Tous les champs", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.copyToClipboardButton": "Copier dans le presse-papiers", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCallout": "{fieldCount, plural, =1 {Ce champ n'est pas défini} other {Ces champs ne sont pas définis}} par la version {version} d'Elastic Common Schema (ECS).", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Mapping de champs personnalisé} other {Mappings de champ personnalisés}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyContent": "Tous les mappings de champs de cet index sont définis par Elastic Common Schema", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyTitle": "Tous les mappings de champs définis par ECS", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customFieldsLabel": "Champs personnalisés", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ Les règles de moteur de détection personnalisées fonctionnent", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ Les règles de moteur de détection fonctionneront pour ces champs", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ Les règles de moteur de détection référençant ces champs ne leur correspondront peut-être pas correctement", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {Le type de mapping d'index et les valeurs de document de ce champ sont conformes} other {Les types de mapping d'index et les valeurs de document de ces champs sont conformes}} à la version {version} d'Elastic Common Schema (ECS)", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Champ conforme} other {Champs conformes}} à ECS", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "Aucun mapping de champ de cet index n'est conforme à Elastic Common Schema (ECS). L'index doit (au moins) contenir un champ de date @timestamp.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyTitle": "Aucun mapping conforme à ECS", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantFieldsLabel": "Champs conformes à ECS", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantMappingsAreFullySupportedMessage": "✅ Les mappings et valeurs de champs conformes à ECS sont totalement pris en charge", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsIsAPermissiveSchemaMessage": "ECS est un schéma permissif. Si vos événements ont des données supplémentaires qui ne peuvent pas être mappées à ECS, vous pouvez tout simplement les ajouter à vos événements à l’aide de noms de champs personnalisés.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsVersionMarkdownComment": "Version Elastic Common Schema (ECS)", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout": "Les champs sont incompatibles avec ECS lorsque les mappings d'index, ou les valeurs des champs de l'index, ne sont pas conformes à la version {version} d'Elastic Common Schema (ECS).", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.fieldsWithMappingsSameFamilyLabel": "Les champs avec des mappings dans la même famille ont exactement le même comportement de recherche que le type défini par ECS, mais ils peuvent avoir une utilisation de l'espace différente ou différentes caractéristiques de performances.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.whenAFieldIsIncompatibleLabel": "Lorsqu'un champ est incompatible :", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Champ incompatible} other {Champs incompatibles}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "Tous les mappings de champs et toutes les valeurs de documents de cet index sont conformes à Elastic Common Schema (ECS).", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "Toutes les valeurs et tous les mappings de champs sont conformes à ECS", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "Index", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ Les mappings ou valeurs de champs qui ne sont pas conformes à ECS ne sont pas pris en charge", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "Veuillez envisager d'ajouter un mapping de champ de @timestamp (date) à cet index, comme requis par Elastic Common Schema (ECS), car :", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCalloutTitle": "Mapping de champ @timestamp (date) manquant pour cet index", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.otherAppCapabilitiesWorkProperlyMessage": "✅ Les autres capacités de l'application fonctionnent correctement", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesDisplayEventsMessage": "✅ Les pages affichent les événements et les champs correctement", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayEventsMessage": "❌ Les pages peuvent ne pas afficher certains événements ou champs en raison de mappings ou valeurs de champs inattendus", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayFieldsMessage": "🌕 Certaines pages et fonctionnalités peuvent ne pas afficher ces champs", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.preBuiltDetectionEngineRulesWorkMessage": "✅ Les règles de moteur de détection préconstruites fonctionnent", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCallout": "{fieldCount, plural, =1 {Ce champ est défini} other {Ces champs sont définis}} par Elastic Common Schema (ECS), version {version}, mais {fieldCount, plural, =1 {son type de mapping de ne correspond} other {leurs types de mapping ne correspondent}} pas exactement.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Mapping de champs} other {Mappings de champ}} de même famille", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyContent": "Tous les mappings de champs et toutes les valeurs de documents de cet index sont conformes à Elastic Common Schema (ECS).", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyTitle": "Toutes les valeurs et tous les mappings de champs sont conformes à ECS", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyTab": "Même famille", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription": "Parfois, les index créés par des intégrations plus anciennes comporteront des mappings ou des valeurs qui étaient conformes, mais ne le sont plus.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "L'index `{indexName}` a des [mappings]({mappingUrl}) ou des valeurs de champ différentes de l'[Elastic Common Schema]({ecsReferenceUrl}) (ECS), [définitions]({ecsFieldReferenceUrl}).de version `{version}`.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "Qualité des données", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 67b07b28089c4..b73b085089dc3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6704,15 +6704,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.checkingLabel": "{index}を確認中", "securitySolutionPackages.ecsDataQualityDashboard.coldDescription": "インデックスは更新されず、頻繁に照会されません。情報はまだ検索可能でなければなりませんが、クエリーが低速でも問題ありません。", "securitySolutionPackages.ecsDataQualityDashboard.coldPatternTooltip": "{pattern}パターンと一致する{indices} {indices, plural, other {インデックス}}{indices, plural, other {は}}コールドです。コールドインデックスは更新されず、ほとんど照会されません。情報はまだ検索可能でなければなりませんが、クエリーが低速でも問題ありません。", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.documentValuesActualColumn": "ドキュメント値(実際)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsDescriptionColumn": "ECS説明", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeColumn": "ECSマッピングタイプ", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeExpectedColumn": "ECSマッピングタイプ(想定)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesColumn": "ECS値", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesExpectedColumn": "ECS値(想定)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.fieldColumn": "フィールド", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeActualColumn": "インデックスマッピングタイプ(実際)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeColumn": "インデックスマッピングタイプ", "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder": "検索フィールド", "securitySolutionPackages.ecsDataQualityDashboard.copyToClipboardButton": "クリップボードにコピー", "securitySolutionPackages.ecsDataQualityDashboard.createADataQualityCaseForIndexHeaderText": "インデックス{indexName}のデータ品質ケースを作成", @@ -6762,8 +6753,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "1つ以上のILMフェーズを選択", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "管理対象外", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "ウォーム", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "非互換フィールドマッピング - {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle": "非互換フィールド値 - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "これらのインデックスライフサイクル管理(ILM)フェーズのインデックスはデータ品質が確認されます", "securitySolutionPackages.ecsDataQualityDashboard.indexNameLabel": "インデックス名", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton": "新しいケースに追加", @@ -6771,44 +6760,31 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyContent": "このインデックスにはマッピングが含まれていません", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyTitle": "マッピングなし", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutTitle": "すべての{fieldCount} {fieldCount, plural, other {個のフィールドマッピング}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allFieldsLabel": "すべてのフィールド", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.copyToClipboardButton": "クリップボードにコピー", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCallout": "{fieldCount, plural, =1 {このフィールドは} other {これらのフィールドは}}Elastic Common Schema(ECS)バージョン{version}によって定義されていません。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCalloutTitle": "{fieldCount}{fieldCount, plural, other {個のカスタムフィールドマッピング}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyContent": "このインデックスのすべてのフィールドマッピングはElastic Common Schemaによって定義されています", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyTitle": "ECSによって定義されたすべてのフィールドマッピング", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customFieldsLabel": "カスタムフィールド", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ カスタム検出エンジンルールが動作する", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ これらのフィールドの検出エンジンルールが動作する", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ これらのフィールドを参照する検出エンジンルールが正常に一致しない場合がある", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {このフィールドのインデックスマッピングタイプとドキュメント値} other {これらのフィールドのインデックスマッピングタイプとドキュメント値}}は、Elastic Common Schema(ECS)バージョン{version}に準拠しています", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠したフィールド}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "このインデックスのどのフィールドマッピングもElastic Common Schema(ECS)と互換性がありません。インデックスには(1つ以上の)@timestamp日付フィールドを含める必要があります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyTitle": "ECS互換マッピングがありません", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantFieldsLabel": "ECS互換フィールド", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantMappingsAreFullySupportedMessage": "✅ ECS互換マッピングおよびフィールド値が完全にサポートされている", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsIsAPermissiveSchemaMessage": "ECSは柔軟なスキーマです。ECSにマッピングできない追加のデータがイベントにある場合は、カスタムフィールド名を使用して、それをイベントに追加できます。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsVersionMarkdownComment": "Elastic Common Schema(ECS)バージョン", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout": "インデックスのマッピングやインデックスのフィールドの値がElastic Common Schema(ECS)、バージョン{version}に準拠していない場合、フィールドはECSと非互換となります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.fieldsWithMappingsSameFamilyLabel": "同じファミリーにマッピングがあるフィールドの検索動作はECSで指定された型とまったく同じですが、スペースの使用量やパフォーマンス特性は異なる場合があります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.whenAFieldIsIncompatibleLabel": "フィールドの互換性がない場合:", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠していないフィールド}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "このインデックスのすべてのフィールドマッピングとドキュメント値がElastic Common Schema(ECS)と互換性があります。", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "すべてのフィールドマッピングと値がECSと互換性があります", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "インデックス", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ ECSと互換性がないマッピングまたはフィールド値はサポートされません", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "次の理由のため、Elastic Common Schema(ECS)で必要な@timestamp(日付)フィールドマッピングをこのインデックスに追加することを検討してください。", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCalloutTitle": "このインデックスの@timestamp(日付)フィールドマッピングが見つかりません", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.otherAppCapabilitiesWorkProperlyMessage": "✅ 他のアプリ機能が正常に動作する", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesDisplayEventsMessage": "✅ ページにイベントとフィールドが正常に表示される", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayEventsMessage": "❌ 予期しないフィールドマッピングまたは値のため、一部のイベントまたはフィールドがページに表示されない場合がある", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayFieldsMessage": "🌕 一部のページと機能にこれらのフィールドが表示されない場合がある", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.preBuiltDetectionEngineRulesWorkMessage": "✅ 構築済み検出エンジンルールが動作する", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCallout": "{fieldCount, plural, =1 {このフィールドは} other {これらのフィールドは}}Elastic Common Schema(ECS)バージョン{version}によって定義されていますが、{fieldCount, plural, other {マッピングタイプ}}は正確に一致しません。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCalloutTitle": "{fieldCount}{fieldCount, plural, other {個の同じファミリーのフィールドマッピング}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyContent": "このインデックスのすべてのフィールドマッピングとドキュメント値がElastic Common Schema(ECS)と互換性があります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyTitle": "すべてのフィールドマッピングと値がECSと互換性があります", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyTab": "同じファミリー", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription": "場合によって、古い統合で作成されたインデックスには、以前あった互換性がなくなったマッピングまたは値が含まれることがあります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "`{indexName}`インデックスには、[Elastic Common Schema]({ecsReferenceUrl})(ECS)バージョン`{version}` [definitions]({ecsFieldReferenceUrl})とは異なる[マッピング]({mappingUrl})またはフィールド値があります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "データ品質", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 500063df288d5..ab2b923985a1b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6718,15 +6718,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.checkingLabel": "正在检查 {index}", "securitySolutionPackages.ecsDataQualityDashboard.coldDescription": "该索引不再进行更新,且不被经常查询。这些信息仍需能够搜索,但查询速度快慢并不重要。", "securitySolutionPackages.ecsDataQualityDashboard.coldPatternTooltip": "{indices} 个匹配 {pattern} 模式的{indices, plural, other {索引}}{indices, plural, other {为}}冷索引。冷索引不再进行更新,且不被经常查询。这些信息仍需能够搜索,但查询速度快慢并不重要。", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.documentValuesActualColumn": "文档值(实际)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsDescriptionColumn": "ECS 描述", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeColumn": "ECS 映射类型", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsMappingTypeExpectedColumn": "ECS 映射类型(预期)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesColumn": "ECS 值", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.ecsValuesExpectedColumn": "ECS 值(预期)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.fieldColumn": "字段", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeActualColumn": "索引映射类型(实际)", - "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.indexMappingTypeColumn": "索引映射类型", "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder": "搜索字段", "securitySolutionPackages.ecsDataQualityDashboard.copyToClipboardButton": "复制到剪贴板", "securitySolutionPackages.ecsDataQualityDashboard.createADataQualityCaseForIndexHeaderText": "为索引 {indexName} 创建数据质量案例", @@ -6776,8 +6767,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "选择一个或多个 ILM 阶段", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "未受管", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "温", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "不兼容的字段映射 - {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle": "不兼容的字段值 - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "将检查具有这些索引生命周期管理 (ILM) 阶段的索引以了解数据质量", "securitySolutionPackages.ecsDataQualityDashboard.indexNameLabel": "索引名称", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton": "添加到新案例", @@ -6785,44 +6774,31 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyContent": "此索引不包含任何映射", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutEmptyTitle": "无映射", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCalloutTitle": "所有 {fieldCount} 个{fieldCount, plural, other {字段映射}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allFieldsLabel": "所有字段", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.copyToClipboardButton": "复制到剪贴板", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCallout": "{fieldCount, plural, =1 {此字段} other {这些字段}}不通过 Elastic Common Schema (ECS) 版本 {version} 来定义。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customCalloutTitle": "{fieldCount} 个定制{fieldCount, plural, other {字段映射}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyContent": "此索引中的所有字段映射均由 Elastic Common Schema 定义", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customEmptyTitle": "由 ECS 字义的所有字段映射", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.customFieldsLabel": "定制字段", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ 定制检测引擎规则有效", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ 检测引擎规则将适用于这些字段", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ 引用这些字段的检测引擎规则可能无法与其正确匹配", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {此字段的索引映射类型和文档值} other {这些字段的索引映射类型和文档值}}遵循 Elastic Common Schema (ECS) 版本 {version}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} 个符合 ECS 规范的{fieldCount, plural, other {字段}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "此索引中没有任何字段映射遵循 Elastic Common Schema (ECS)。此索引必须(至少)包含一个 @timestamp 日期字段。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyTitle": "没有符合 ECS 规范的映射", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantFieldsLabel": "符合 ECS 规范的字段", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantMappingsAreFullySupportedMessage": "✅ 完全支持符合 ECS 规范的映射和字段值", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsIsAPermissiveSchemaMessage": "ECS 是一种允许使用的架构。如果您的事件具有其他无法映射为 ECS 的数据,您可以使用定制字段名称直接将其添加到事件中。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsVersionMarkdownComment": "Elastic Common Schema (ECS) 版本", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout": "索引映射或索引中字段的值未遵循 Elastic Common Schema (ECS) 版本 {version} 时,字段将与 ECS 不兼容。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.fieldsWithMappingsSameFamilyLabel": "同一系列中包含映射的字段具有与由 ECS 指定的类型完全相同的搜索行为,但工作区使用情况和性能特征可能会有所不同。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout.whenAFieldIsIncompatibleLabel": "当字段不兼容时:", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} 个不兼容的{fieldCount, plural, other {字段}}", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "此索引中的所有字段映射和文档值均符合 Elastic Common Schema (ECS) 规范。", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "所有字段映射和值均符合 ECS 规范", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "索引", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ 不支持不符合 ECS 规范的映射或字段值", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "考虑根据 Elastic Common Schema (ECS) 的要求将 @timestamp(日期)字段映射添加到此索引,因为:", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCalloutTitle": "缺少此索引的 @timestamp(日期)字段映射", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.otherAppCapabilitiesWorkProperlyMessage": "✅ 其他应用功能正常运行", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesDisplayEventsMessage": "✅ 页面正确显示事件和字段", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayEventsMessage": "❌ 由于出现意外的字段映射或值,页面可能不会显示某些事件或字段", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.pagesMayNotDisplayFieldsMessage": "🌕 某些页面和功能可能不会显示这些字段", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.preBuiltDetectionEngineRulesWorkMessage": "✅ 预构建的检测引擎规则有效", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCallout": "{fieldCount, plural, =1 {此字段} other {这些字段}}由 Elastic Common Schema (ECS) 版本 {version} 定义,但{fieldCount, plural, other {其映射类型不}}完全匹配。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyCalloutTitle": "{fieldCount} 个同一系列的{fieldCount, plural, other {字段映射}}", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyContent": "此索引中的所有字段映射和文档值均符合 Elastic Common Schema (ECS) 规范。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyEmptyTitle": "所有字段映射和值均符合 ECS 规范", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sameFamilyTab": "同一系列", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription": "有时候,用较旧集成创建的索引的映射或值可能过去符合规范,但现在不再符合。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "`{indexName}` 索引具有与 [Elastic Common Schema] ({ecsReferenceUrl}) (ECS) 版本 `{version}` [定义]({ecsFieldReferenceUrl}) 不同的[映射]({mappingUrl}) 或字段值。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "数据质量",