diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx index 07af65877da01..97b633eff4a64 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx @@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { mockAllowedValues } from '../../mock/allowed_values/mock_allowed_values'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { EcsAllowedValues } from '.'; describe('EcsAllowedValues', () => { describe('when `allowedValues` exists', () => { beforeEach(() => { render( - + - + ); }); @@ -36,9 +36,9 @@ describe('EcsAllowedValues', () => { describe('when `allowedValues` is undefined', () => { beforeEach(() => { render( - + - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.tsx index 92ad91e91b4cf..8f63047bb91e1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.tsx @@ -7,16 +7,11 @@ import { EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; import { EMPTY_PLACEHOLDER } from '../helpers'; import { CodeSuccess } from '../../styles'; import type { AllowedValue } from '../../types'; -const EcsAllowedValueFlexItem = styled(EuiFlexItem)` - margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; -`; - interface Props { allowedValues: AllowedValue[] | undefined; } @@ -25,11 +20,11 @@ const EcsAllowedValuesComponent: React.FC = ({ allowedValues }) => allowedValues == null ? ( {EMPTY_PLACEHOLDER} ) : ( - + {allowedValues.map((x, i) => ( - + {x.name} - + ))} ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx index 1dad2a79d9651..1695f2587a674 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx @@ -15,7 +15,7 @@ import { someField, eventCategoryWithUnallowedValues, } from '../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { DOCUMENT_VALUES_ACTUAL, ECS_DESCRIPTION, @@ -30,7 +30,7 @@ import { EMPTY_PLACEHOLDER, getCommonTableColumns } from '.'; describe('getCommonTableColumns', () => { test('it returns the expected column configuration', () => { expect(getCommonTableColumns().map((x) => omit('render', x))).toEqual([ - { field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '20%' }, + { field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '15%' }, { field: 'type', name: ECS_MAPPING_TYPE_EXPECTED, @@ -64,7 +64,7 @@ describe('getCommonTableColumns', () => { name: ECS_DESCRIPTION, sortable: false, truncateText: false, - width: '20%', + width: '25%', }, ]); }); @@ -76,9 +76,9 @@ describe('getCommonTableColumns', () => { const expected = 'keyword'; render( - + {typeColumnRender != null && typeColumnRender(eventCategory.type, eventCategory)} - + ); expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected); @@ -89,9 +89,9 @@ describe('getCommonTableColumns', () => { const typeColumnRender = columns[1].render; render( - + {typeColumnRender != null && typeColumnRender(undefined, eventCategory)} - + ); expect(screen.getByTestId('codeSuccess')).toHaveTextContent(EMPTY_PLACEHOLDER); @@ -113,13 +113,13 @@ describe('getCommonTableColumns', () => { }; render( - + {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender( withTypeMismatchSameFamily.indexFieldType, withTypeMismatchSameFamily )} - + ); }); @@ -146,13 +146,13 @@ describe('getCommonTableColumns', () => { }; render( - + {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender( withTypeMismatchDifferentFamily.indexFieldType, withTypeMismatchDifferentFamily )} - + ); expect(screen.getByTestId('codeDanger')).toHaveTextContent(indexFieldType); @@ -165,10 +165,10 @@ describe('getCommonTableColumns', () => { const indexFieldTypeColumnRender = columns[2].render; render( - + {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender(eventCategory.indexFieldType, eventCategory)} - + ); expect(screen.getByTestId('codeSuccess')).toHaveTextContent(eventCategory.indexFieldType); @@ -187,10 +187,10 @@ describe('getCommonTableColumns', () => { : 'unexpected'; render( - + {allowedValuesolumnRender != null && allowedValuesolumnRender(eventCategory.allowed_values, eventCategory)} - + ); expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent(expectedAllowedValuesNames); @@ -206,10 +206,10 @@ describe('getCommonTableColumns', () => { }; render( - + {allowedValuesolumnRender != null && allowedValuesolumnRender(undefined, withUndefinedAllowedValues)} - + ); expect(screen.getByTestId('ecsAllowedValuesEmpty')).toHaveTextContent(EMPTY_PLACEHOLDER); @@ -222,13 +222,13 @@ describe('getCommonTableColumns', () => { const indexInvalidValuesRender = columns[4].render; render( - + {indexInvalidValuesRender != null && indexInvalidValuesRender( eventCategoryWithUnallowedValues.indexInvalidValues, eventCategoryWithUnallowedValues )} - + ); expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( @@ -249,10 +249,10 @@ describe('getCommonTableColumns', () => { }; render( - + {descriptionolumnRender != null && descriptionolumnRender(withDescription.description, withDescription)} - + ); expect(screen.getByTestId('description')).toHaveTextContent(expectedDescription); @@ -263,9 +263,9 @@ describe('getCommonTableColumns', () => { const descriptionolumnRender = columns[5].render; render( - + {descriptionolumnRender != null && descriptionolumnRender(undefined, someField)} - + ); expect(screen.getByTestId('emptyDescription')).toHaveTextContent(EMPTY_PLACEHOLDER); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.tsx index 2e0d2506d6e0f..61762d6fc8182 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.tsx @@ -27,7 +27,7 @@ export const getCommonTableColumns = (): Array< name: i18n.FIELD, sortable: true, truncateText: false, - width: '20%', + width: '15%', }, { field: 'type', @@ -98,6 +98,6 @@ export const getCommonTableColumns = (): Array< ), sortable: false, truncateText: false, - width: '20%', + width: '25%', }, ]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx index 367ff38fa1093..8a9272a717217 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx @@ -10,7 +10,7 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +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 '.'; @@ -25,7 +25,7 @@ describe('getIncompatibleMappingsTableColumns', () => { name: 'Field', sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'type', @@ -46,7 +46,7 @@ describe('getIncompatibleMappingsTableColumns', () => { name: 'ECS description', sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]); }); @@ -58,9 +58,9 @@ describe('getIncompatibleMappingsTableColumns', () => { const expected = 'keyword'; render( - + {typeColumnRender != null && typeColumnRender(eventCategory.type, eventCategory)} - + ); expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected); @@ -82,13 +82,13 @@ describe('getIncompatibleMappingsTableColumns', () => { }; render( - + {indexFieldTypeColumnRender != null && indexFieldTypeColumnRender( withTypeMismatchSameFamily.indexFieldType, withTypeMismatchSameFamily )} - + ); }); @@ -115,13 +115,13 @@ describe('getIncompatibleMappingsTableColumns', () => { }; 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/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx index 70078ab7ccc16..4592e22565d74 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.tsx @@ -23,7 +23,7 @@ export const getIncompatibleMappingsTableColumns = (): Array< name: i18n.FIELD, sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'type', @@ -54,6 +54,6 @@ export const getIncompatibleMappingsTableColumns = (): Array< name: i18n.ECS_DESCRIPTION, sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx index d9897cfa3d399..2bb1e72c8ac6a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx @@ -19,7 +19,7 @@ import { eventCategoryWithUnallowedValues, someField, } from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestProviders } from '../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../mock/test_providers/test_providers'; describe('helpers', () => { describe('getCustomTableColumns', () => { @@ -48,12 +48,12 @@ describe('helpers', () => { const indexFieldTypeRender = columns[1].render; render( - + <> {indexFieldTypeRender != null && indexFieldTypeRender(someField.indexFieldType, someField)} - + ); expect(screen.getByTestId('indexFieldType')).toHaveTextContent(someField.indexFieldType); @@ -69,7 +69,7 @@ describe('helpers', () => { name: 'Field', sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'type', @@ -90,7 +90,7 @@ describe('helpers', () => { name: 'ECS description', sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]); }); @@ -102,9 +102,9 @@ describe('helpers', () => { const typeRender = columns[1].render; render( - + <>{typeRender != null && typeRender(eventCategory.type, eventCategory)} - + ); }); @@ -125,12 +125,12 @@ describe('helpers', () => { const allowedValuesRender = columns[2].render; render( - + <> {allowedValuesRender != null && allowedValuesRender(eventCategory.allowed_values, eventCategory)} - + ); }); @@ -156,7 +156,7 @@ describe('helpers', () => { const allowedValuesRender = columns[2].render; render( - + <> {allowedValuesRender != null && allowedValuesRender( @@ -164,7 +164,7 @@ describe('helpers', () => { withUndefinedAllowedValues )} - + ); }); @@ -185,12 +185,12 @@ describe('helpers', () => { const descriptionRender = columns[3].render; render( - + <> {descriptionRender != null && descriptionRender(eventCategory.description, eventCategory)} - + ); }); @@ -215,7 +215,7 @@ describe('helpers', () => { name: 'Field', sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'allowed_values', @@ -236,7 +236,7 @@ describe('helpers', () => { name: 'ECS description', sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]); }); @@ -248,12 +248,12 @@ describe('helpers', () => { const allowedValuesRender = columns[1].render; render( - + <> {allowedValuesRender != null && allowedValuesRender(eventCategory.allowed_values, eventCategory)} - + ); }); @@ -279,7 +279,7 @@ describe('helpers', () => { const allowedValuesRender = columns[1].render; render( - + <> {allowedValuesRender != null && allowedValuesRender( @@ -287,7 +287,7 @@ describe('helpers', () => { withUndefinedAllowedValues )} - + ); }); @@ -308,7 +308,7 @@ describe('helpers', () => { const indexInvalidValuesRender = columns[2].render; render( - + <> {indexInvalidValuesRender != null && indexInvalidValuesRender( @@ -316,7 +316,7 @@ describe('helpers', () => { eventCategoryWithUnallowedValues )} - + ); }); @@ -337,12 +337,12 @@ describe('helpers', () => { const indexInvalidValuesRender = columns[2].render; render( - + <> {indexInvalidValuesRender != null && indexInvalidValuesRender(eventCategory.indexInvalidValues, eventCategory)} - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.tsx index a9f5c17034833..2563ced06733c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/helpers.tsx @@ -52,7 +52,7 @@ export const getEcsCompliantTableColumns = (): Array< name: i18n.FIELD, sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'type', @@ -78,7 +78,7 @@ export const getEcsCompliantTableColumns = (): Array< render: (description: string) => {description}, sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]; @@ -90,7 +90,7 @@ export const getIncompatibleValuesTableColumns = (): Array< name: i18n.FIELD, sortable: true, truncateText: false, - width: '25%', + width: '15%', }, { field: 'allowed_values', @@ -117,6 +117,6 @@ export const getIncompatibleValuesTableColumns = (): Array< name: i18n.ECS_DESCRIPTION, sortable: false, truncateText: false, - width: '25%', + width: '35%', }, ]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index.test.tsx index 8732f27702bc2..f1dcced85619f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../data_quality_panel/tabs/incompatible_tab/translations'; import { eventCategory } from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestProviders } from '../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../mock/test_providers/test_providers'; import { CompareFieldsTable } from '.'; import { getIncompatibleMappingsTableColumns } from './get_incompatible_mappings_table_columns'; @@ -18,13 +18,13 @@ describe('CompareFieldsTable', () => { describe('rendering', () => { beforeEach(() => { render( - + - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.test.tsx index 6a93d3f90a24d..719c94bd85d58 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.test.tsx @@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { EMPTY_PLACEHOLDER } from '../helpers'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { UnallowedValueCount } from '../../types'; import { IndexInvalidValues } from '.'; describe('IndexInvalidValues', () => { test('it renders a placeholder with the expected content when `indexInvalidValues` is empty', () => { render( - + - + ); expect(screen.getByTestId('emptyPlaceholder')).toHaveTextContent(EMPTY_PLACEHOLDER); @@ -37,9 +37,9 @@ describe('IndexInvalidValues', () => { ]; render( - + - + ); expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/constants.ts index a53c50edc1084..c89c4dc5daa15 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/constants.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/constants.ts @@ -6,7 +6,35 @@ */ import { EcsFlat } from '@elastic/ecs'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; + import { EcsFieldMetadata } from './types'; +import * as i18n from './translations'; export const EcsFlatTyped = EcsFlat as unknown as Record; export type EcsFlatTyped = typeof EcsFlatTyped; + +export const ilmPhaseOptionsStatic: EuiComboBoxOptionOption[] = [ + { + label: i18n.HOT, + value: 'hot', + }, + { + label: i18n.WARM, + value: 'warm', + }, + { + disabled: true, + label: i18n.COLD, + value: 'cold', + }, + { + disabled: true, + label: i18n.FROZEN, + value: 'frozen', + }, + { + label: i18n.UNMANAGED, + value: 'unmanaged', + }, +]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/indices_check_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/indices_check_context/index.tsx new file mode 100644 index 0000000000000..d6ecacf09ac4e --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/indices_check_context/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { UseIndicesCheckReturnValue } from '../../use_indices_check/types'; + +export const IndicesCheckContext = createContext(null); + +export const useIndicesCheckContext = () => { + const context = useContext(IndicesCheckContext); + if (context == null) { + throw new Error('useIndicesCheckContext must be used inside the IndicesCheckContextProvider.'); + } + return context; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/results_rollup_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/results_rollup_context/index.tsx new file mode 100644 index 0000000000000..7771a06fbf7f8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/contexts/results_rollup_context/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext, useContext } from 'react'; + +import { UseResultsRollupReturnValue } from '../../use_results_rollup/types'; + +export const ResultsRollupContext = createContext(null); + +export const useResultsRollupContext = () => { + const context = useContext(ResultsRollupContext); + if (context == null) { + throw new Error( + 'useResultsRollupContext must be used inside the ResultsRollupContextProvider.' + ); + } + return context; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.test.tsx new file mode 100644 index 0000000000000..1fec0d39fbfd0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; + +import { AddToNewCaseAction } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; +import userEvent from '@testing-library/user-event'; + +describe('AddToNewCaseAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render add to new case link', () => { + render( + + + + + + ); + + expect(screen.getByTestId('addToNewCase')).toHaveTextContent('Add to new case'); + }); + + describe('when ilm phases are not provided', () => { + it('should render disabled add to new case link', () => { + render( + + + + + + ); + + expect(screen.getByTestId('addToNewCase')).toBeDisabled(); + }); + }); + + describe('when createAndReadCases() returns false', () => { + it('should render disabled add to new case link', () => { + render( + + false }} + > + + + + ); + + expect(screen.getByTestId('addToNewCase')).toBeDisabled(); + }); + }); + + describe('when clicking on add to new case link', () => { + it('should open create case flyout with header content and provided markdown', () => { + let headerContent: React.ReactNode = null; + const openCreateCaseFlyout = jest.fn(({ headerContent: _headerContent }) => { + headerContent = render(_headerContent).container; + }); + render( + + + + + + ); + + userEvent.click(screen.getByTestId('addToNewCase')); + + expect(openCreateCaseFlyout).toHaveBeenCalledWith({ + comments: ['test markdown'], + headerContent: expect.anything(), + }); + + expect(headerContent).toContainHTML('
Create a data quality case
'); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.tsx new file mode 100644 index 0000000000000..537f5b13a0f08 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/add_to_new_case/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiIcon, EuiLink } from '@elastic/eui'; + +import { useDataQualityContext } from '../../data_quality_context'; +import { useAddToNewCase } from '../../../use_add_to_new_case'; +import { StyledLinkText } from '../styles'; +import { ADD_TO_NEW_CASE } from '../../../translations'; + +interface Props { + markdownComment: string; +} + +const AddToNewCaseActionComponent: React.FC = ({ markdownComment }) => { + const { canUserCreateAndReadCases, openCreateCaseFlyout, ilmPhases } = useDataQualityContext(); + const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({ + canUserCreateAndReadCases, + openCreateCaseFlyout, + }); + + const onClickAddToCase = useCallback( + () => onAddToNewCase([markdownComment]), + [markdownComment, onAddToNewCase] + ); + + const addToNewCaseContextMenuOnClick = useCallback(() => { + onClickAddToCase(); + }, [onClickAddToCase]); + + const disableAll = ilmPhases.length === 0; + + return ( + + + + {ADD_TO_NEW_CASE} + + + ); +}; + +AddToNewCaseActionComponent.displayName = 'AddToNewCaseActionComponent'; + +export const AddToNewCaseAction = React.memo(AddToNewCaseActionComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.test.tsx new file mode 100644 index 0000000000000..8d706aad92f1f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; + +import { ChatAction } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; + +describe('ChatAction', () => { + it('should render new chat link', () => { + render( + + + + + + ); + + expect(screen.getByTestId('newChatLink')).toHaveTextContent('Ask Assistant'); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.tsx new file mode 100644 index 0000000000000..ca37bcaad97d5 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback } from 'react'; +import { AssistantAvatar, NewChat } from '@kbn/elastic-assistant'; +import styled from 'styled-components'; + +import { + DATA_QUALITY_DASHBOARD_CONVERSATION_ID, + DATA_QUALITY_PROMPT_CONTEXT_PILL, + DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP, + DATA_QUALITY_SUGGESTED_USER_PROMPT, +} from '../../../translations'; +import { useDataQualityContext } from '../../data_quality_context'; +import { ASK_ASSISTANT } from './translations'; + +const StyledLinkText = styled.span` + display: flex; + gap: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +interface Props { + markdownComment: string; + indexName: string; +} + +const ChatActionComponent: FC = ({ indexName, markdownComment }) => { + const { isAssistantEnabled } = useDataQualityContext(); + const getPromptContext = useCallback(async () => markdownComment, [markdownComment]); + return ( + + + + {ASK_ASSISTANT} + + + ); +}; + +ChatActionComponent.displayName = 'ChatActionComponent'; + +export const ChatAction = React.memo(ChatActionComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/translations.ts similarity index 57% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/translations.ts index 07337268b7d8c..245521c069bf6 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/chat/translations.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const DATA_QUALITY_DASHBOARD_CONVERSATION_ID = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.dataQualityDashboardConversationId', +export const ASK_ASSISTANT = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.actions.askAssistant', { - defaultMessage: 'Data Quality dashboard', + defaultMessage: 'Ask Assistant', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.test.tsx new file mode 100644 index 0000000000000..6e2147293237a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { copyToClipboard } from '@elastic/eui'; + +import { CopyToClipboardAction } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + copyToClipboard: jest.fn(), + }; +}); + +describe('CopyToClipboardAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render a copy to clipboard link', () => { + render( + + + + + + ); + + expect(screen.getByTestId('copyToClipboard')).toHaveTextContent('Copy to clipboard'); + }); + + describe('when ilm phases are not provided', () => { + it('should render disabled copy to clipboard link', () => { + render( + + + + + + ); + + expect(screen.getByTestId('copyToClipboard')).toBeDisabled(); + }); + }); + + describe('when copy to clipboard is clicked', () => { + it('should copy the markdown comment to the clipboard and add success toast', () => { + const addSuccessToast = jest.fn(); + render( + + + + + + ); + + userEvent.click(screen.getByTestId('copyToClipboard')); + + expect(copyToClipboard).toHaveBeenCalledWith('test comment'); + expect(addSuccessToast).toHaveBeenCalledWith({ title: 'Copied results to the clipboard' }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.tsx new file mode 100644 index 0000000000000..abf85203a0db1 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/copy_to_clipboard/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiIcon, EuiLink, copyToClipboard } from '@elastic/eui'; + +import { useDataQualityContext } from '../../data_quality_context'; +import { COPIED_RESULTS_TOAST_TITLE, COPY_TO_CLIPBOARD } from '../../../translations'; +import { StyledLinkText } from '../styles'; + +interface Props { + markdownComment: string; +} + +const CopyToClipboardActionComponent: React.FC = ({ markdownComment }) => { + const { addSuccessToast, ilmPhases } = useDataQualityContext(); + const onCopy = useCallback(() => { + copyToClipboard(markdownComment); + + addSuccessToast({ + title: COPIED_RESULTS_TOAST_TITLE, + }); + }, [addSuccessToast, markdownComment]); + + return ( + + + + {COPY_TO_CLIPBOARD} + + + ); +}; + +CopyToClipboardActionComponent.displayName = 'CopyToClipboardActionComponent'; + +export const CopyToClipboardAction = React.memo(CopyToClipboardActionComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.test.tsx new file mode 100644 index 0000000000000..55f32280ddb9d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { Actions } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; + +describe('Actions', () => { + it('renders nothing by default', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + describe('given showAddToNewCaseAction is true', () => { + it('renders AddToNewCaseAction', () => { + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId('addToNewCase')).toBeInTheDocument(); + }); + }); + + describe('given showCopyToClipboardAction is true', () => { + it('renders CopyToClipboardAction', () => { + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId('copyToClipboard')).toBeInTheDocument(); + }); + }); + + describe('given showChatAction is true and indexName is present', () => { + it('renders ChatAction', () => { + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId('newChatLink')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.tsx new file mode 100644 index 0000000000000..b1f4fa5911097 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/index.tsx @@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { ChatAction } from './chat'; +import { CopyToClipboardAction } from './copy_to_clipboard'; +import { AddToNewCaseAction } from './add_to_new_case'; + +export interface Props { + markdownComment: string; + indexName?: string; + showAddToNewCaseAction?: boolean; + showCopyToClipboardAction?: boolean; + showChatAction?: boolean; +} + +const ActionsComponent: React.FC = ({ + showAddToNewCaseAction, + showCopyToClipboardAction, + showChatAction, + markdownComment, + indexName, +}) => { + if (!showAddToNewCaseAction && !showCopyToClipboardAction && !showChatAction) { + return null; + } + + return ( + + {showAddToNewCaseAction && ( + + + + )} + + {showCopyToClipboardAction && ( + + {' '} + {' '} + + )} + + {showChatAction && indexName && ( + + + + )} + + ); +}; + +ActionsComponent.displayName = 'ActionsComponent'; + +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/styles.tsx similarity index 55% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/styles.tsx index 3765bbe68ed16..4319efa6b1304 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/actions/styles.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import type { PartitionedFieldMetadata } from '../types'; +import styled from 'styled-components'; -export const allMetadataIsEmpty = (partitionedFieldMetadata: PartitionedFieldMetadata): boolean => - partitionedFieldMetadata.all.length === 0; +export const StyledLinkText = styled.span` + display: flex; + gap: ${({ theme }) => theme.eui.euiSizeS}; +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx index 78ae37d4fea64..399b5705d028d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx @@ -5,65 +5,16 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; -import numeral from '@elastic/numeral'; import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { EMPTY_STAT } from '../../../helpers'; -import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; -import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; -import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; -import { PatternRollup } from '../../../types'; -import { Props, DataQualityDetails } from '.'; - -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; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; +import { DataQualityDetails } from '.'; const ilmPhases = ['hot', 'warm', 'unmanaged']; -const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; - -const patternRollups: Record = { - '.alerts-security.alerts-default': alertIndexWithAllResults, - 'auditbeat-*': auditbeatWithAllResults, - 'packetbeat-*': packetbeatNoResults, -}; - -const patternIndexNames: Record = { - 'auditbeat-*': [ - '.ds-auditbeat-8.6.1-2023.02.07-000001', - 'auditbeat-custom-empty-index-1', - 'auditbeat-custom-index-1', - ], - '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], - 'packetbeat-*': [ - '.ds-packetbeat-8.5.3-2023.02.04-000001', - '.ds-packetbeat-8.6.1-2023.02.04-000001', - ], -}; - -const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: jest.fn(), - formatBytes, - formatNumber, - getGroupByFieldsOnClick: jest.fn(), - ilmPhases, - isAssistantEnabled: true, - openCreateCaseFlyout: jest.fn(), - patternIndexNames, - patternRollups, - patterns, - baseTheme: DARK_THEME, - updatePatternIndexNames: jest.fn(), - updatePatternRollup: jest.fn(), -}; describe('DataQualityDetails', () => { describe('when ILM phases are provided', () => { @@ -71,9 +22,11 @@ describe('DataQualityDetails', () => { jest.clearAllMocks(); render( - - - + + + + + ); await waitFor(() => {}); // wait for PatternComponent state updates @@ -93,9 +46,11 @@ describe('DataQualityDetails', () => { jest.clearAllMocks(); render( - - - + + + + + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx index 9b2dff03e30c1..8f8686f0c03b2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx @@ -5,93 +5,20 @@ * 2.0. */ -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; - import React, { useCallback, useState } from 'react'; import { IlmPhasesEmptyPrompt } from '../../../ilm_phases_empty_prompt'; import { IndicesDetails } from './indices_details'; import { StorageDetails } from './storage_details'; -import { PatternRollup, SelectedIndex } from '../../../types'; +import { SelectedIndex } from '../../../types'; import { useDataQualityContext } from '../../data_quality_context'; -export interface Props { - addSuccessToast: (toast: { title: string }) => void; - baseTheme: Theme; - canUserCreateAndReadCases: () => boolean; - endDate?: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - ilmPhases: string[]; - isAssistantEnabled: boolean; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; - patternIndexNames: Record; - patternRollups: Record; - patterns: string[]; - startDate?: string | null; - theme?: PartialTheme; - updatePatternIndexNames: ({ - indexNames, - pattern, - }: { - indexNames: string[]; - pattern: string; - }) => void; - updatePatternRollup: (patternRollup: PatternRollup) => void; -} - -const DataQualityDetailsComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - endDate, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, - ilmPhases, - isAssistantEnabled, - openCreateCaseFlyout, - patternIndexNames, - patternRollups, - patterns, - startDate, - theme, - baseTheme, - updatePatternIndexNames, - updatePatternRollup, -}) => { - const { isILMAvailable } = useDataQualityContext(); - const [selectedIndex, setSelectedIndex] = useState(null); +const DataQualityDetailsComponent: React.FC = () => { + const { isILMAvailable, ilmPhases } = useDataQualityContext(); + const [chartSelectedIndex, setChartSelectedIndex] = useState(null); - const onIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => { - setSelectedIndex({ indexName, pattern }); + const handleChartsIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => { + setChartSelectedIndex({ indexName, pattern }); }, []); if (isILMAvailable && ilmPhases.length === 0) { @@ -100,37 +27,10 @@ const DataQualityDetailsComponent: React.FC = ({ return ( <> - - + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx index 672f5ed76b664..ddb17a64d1fc2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; import numeral from '@elastic/numeral'; import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; @@ -14,7 +13,10 @@ import { EMPTY_STAT } from '../../../../helpers'; import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../mock/test_providers/test_providers'; import { PatternRollup } from '../../../../types'; import { Props, IndicesDetails } from '.'; @@ -49,22 +51,8 @@ const patternIndexNames: Record = { }; const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: jest.fn(), - formatBytes, - formatNumber, - getGroupByFieldsOnClick: jest.fn(), - ilmPhases, - isAssistantEnabled: true, - openCreateCaseFlyout: jest.fn(), - patternIndexNames, - patternRollups, - patterns, - selectedIndex: null, - setSelectedIndex: jest.fn(), - baseTheme: DARK_THEME, - updatePatternIndexNames: jest.fn(), - updatePatternRollup: jest.fn(), + chartSelectedIndex: null, + setChartSelectedIndex: jest.fn(), }; describe('IndicesDetails', () => { @@ -72,9 +60,14 @@ describe('IndicesDetails', () => { jest.clearAllMocks(); render( - - - + + + + + ); await waitFor(() => {}); @@ -87,10 +80,4 @@ describe('IndicesDetails', () => { }); }); }); - - describe('rendering spacers', () => { - test('it renders the expected number of spacers', () => { - expect(screen.getAllByTestId('bodyPatternSpacer')).toHaveLength(patterns.length - 1); - }); - }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx index 3bfbe9065ebea..426e61513ee14 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx @@ -5,118 +5,51 @@ * 2.0. */ -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import styled from 'styled-components'; +import { useResultsRollupContext } from '../../../../contexts/results_rollup_context'; import { Pattern } from '../../../pattern'; -import { PatternRollup, SelectedIndex } from '../../../../types'; +import { SelectedIndex } from '../../../../types'; +import { useDataQualityContext } from '../../../data_quality_context'; + +const StyledPatternWrapperFlexItem = styled(EuiFlexItem)` + margin-bottom: ${({ theme }) => theme.eui.euiSize}; + + &:last-child { + margin-bottom: 0; + } +`; export interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - endDate?: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - ilmPhases: string[]; - isAssistantEnabled: boolean; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; - patternIndexNames: Record; - patternRollups: Record; - patterns: string[]; - selectedIndex: SelectedIndex | null; - setSelectedIndex: (selectedIndex: SelectedIndex | null) => void; - startDate?: string | null; - theme?: PartialTheme; - baseTheme: Theme; - updatePatternIndexNames: ({ - indexNames, - pattern, - }: { - indexNames: string[]; - pattern: string; - }) => void; - updatePatternRollup: (patternRollup: PatternRollup) => void; + chartSelectedIndex: SelectedIndex | null; + setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void; } const IndicesDetailsComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - endDate, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, - ilmPhases, - isAssistantEnabled, - openCreateCaseFlyout, - patternIndexNames, - patternRollups, - patterns, - selectedIndex, - setSelectedIndex, - startDate, - theme, - baseTheme, - updatePatternIndexNames, - updatePatternRollup, -}) => ( -
- {patterns.map((pattern, i) => ( - - - {patterns[i + 1] && } - - ))} -
-); + chartSelectedIndex, + setChartSelectedIndex, +}) => { + const { patternRollups, patternIndexNames } = useResultsRollupContext(); + const { patterns } = useDataQualityContext(); + + return ( +
+ {patterns.map((pattern) => ( + + + + ))} +
+ ); +}; IndicesDetailsComponent.displayName = 'IndicesDetailsComponent'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx index a3ff3665cf6ed..5dd06ad340474 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; import numeral from '@elastic/numeral'; import { render, screen } from '@testing-library/react'; import React from 'react'; @@ -14,7 +13,10 @@ import { EMPTY_STAT } from '../../../../helpers'; import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../mock/test_providers/test_providers'; import { PatternRollup } from '../../../../types'; import { Props, StorageDetails } from '.'; @@ -38,13 +40,7 @@ const patternRollups: Record = { const onIndexSelected = jest.fn(); const defaultProps: Props = { - formatBytes, - formatNumber, - ilmPhases, onIndexSelected, - patternRollups, - patterns, - baseTheme: DARK_THEME, }; describe('StorageDetails', () => { @@ -52,9 +48,14 @@ describe('StorageDetails', () => { jest.clearAllMocks(); render( - - - + + + + + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx index a6a96e1b23aa2..ff43116c02878 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx @@ -5,38 +5,23 @@ * 2.0. */ -import type { PartialTheme, Theme } from '@elastic/charts'; import React, { useCallback, useMemo } from 'react'; +import { useResultsRollupContext } from '../../../../contexts/results_rollup_context'; import { getFlattenedBuckets } from './helpers'; import { StorageTreemap } from '../../../storage_treemap'; -import { DEFAULT_MAX_CHART_HEIGHT, StorageTreemapContainer } from '../../../tabs/styles'; -import { PatternRollup, SelectedIndex } from '../../../../types'; +import { DEFAULT_MAX_CHART_HEIGHT } from '../../../tabs/styles'; +import { SelectedIndex } from '../../../../types'; import { useDataQualityContext } from '../../../data_quality_context'; import { DOCS_UNIT } from './translations'; export interface Props { - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhases: string[]; onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; - patternRollups: Record; - patterns: string[]; - theme?: PartialTheme; - baseTheme: Theme; } -const StorageDetailsComponent: React.FC = ({ - formatBytes, - formatNumber, - ilmPhases, - onIndexSelected, - patternRollups, - patterns, - theme, - baseTheme, -}) => { - const { isILMAvailable } = useDataQualityContext(); +const StorageDetailsComponent: React.FC = ({ onIndexSelected }) => { + const { patternRollups } = useResultsRollupContext(); + const { isILMAvailable, ilmPhases, formatBytes, formatNumber } = useDataQualityContext(); const flattenedBuckets = useMemo( () => @@ -55,19 +40,16 @@ const StorageDetailsComponent: React.FC = ({ ); return ( - +
- +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx index 1b8dad1c42ef7..595c17e16faaa 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx @@ -5,44 +5,23 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; -import numeral from '@elastic/numeral'; import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { EMPTY_STAT } from '../../helpers'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import { Body } from '.'; -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 ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; - describe('IndexInvalidValues', () => { test('it renders the data quality summary', async () => { render( - - - + + + + + ); await waitFor(() => { @@ -53,57 +32,20 @@ describe('IndexInvalidValues', () => { describe('patterns', () => { const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'logs-*', 'packetbeat-*']; - patterns.forEach((pattern) => { - test(`it renders the '${pattern}' pattern`, async () => { - render( - - - - ); + test(`it renders the '${patterns.join(', ')}' patterns`, async () => { + render( + + + + + + ); + for (const pattern of patterns) { await waitFor(() => { expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument(); }); - }); - }); - - test('it renders the expected number of spacers', async () => { - render( - - - - ); - - const items = await screen.findAllByTestId('bodyPatternSpacer'); - await waitFor(() => { - expect(items).toHaveLength(patterns.length - 1); - }); + } }); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx index c36e34fc803e2..a8ceecdc76c09 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx @@ -5,137 +5,22 @@ * 2.0. */ -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { DataQualityDetails } from './data_quality_details'; import { DataQualitySummary } from '../data_quality_summary'; -import { useResultsRollup } from '../../use_results_rollup'; - -interface Props { - addSuccessToast: (toast: { title: string }) => void; - baseTheme: Theme; - canUserCreateAndReadCases: () => boolean; - endDate?: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - ilmPhases: string[]; - isAssistantEnabled: boolean; - lastChecked: string; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; - patterns: string[]; - setLastChecked: (lastChecked: string) => void; - startDate?: string | null; - theme?: PartialTheme; -} - -const BodyComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - endDate, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, - ilmPhases, - isAssistantEnabled, - lastChecked, - openCreateCaseFlyout, - patterns, - setLastChecked, - startDate, - theme, - baseTheme, -}) => { - const { - onCheckCompleted, - patternIndexNames, - patternRollups, - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - totalSizeInBytes, - updatePatternIndexNames, - updatePatternRollup, - } = useResultsRollup({ - ilmPhases, - patterns, - }); +const BodyComponent: React.FC = () => { return ( - + - + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx index 43cc7ec6051c6..fb8eccd4b7f8a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { Theme } from '@elastic/charts'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { renderHook } from '@testing-library/react-hooks'; import React, { FC, PropsWithChildren } from 'react'; @@ -25,6 +26,45 @@ const ContextWrapper: FC> = ({ children }) => ( telemetryEvents={mockTelemetryEvents} isILMAvailable={true} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} @@ -43,9 +83,9 @@ describe('DataQualityContext', () => { ); }); - test('it should return the httpFetch function', async () => { + test('it should return the httpFetch function', () => { const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper }); - const httpFetch = await result.current.httpFetch; + const httpFetch = result.current.httpFetch; const path = '/path/to/resource'; httpFetch(path); @@ -53,16 +93,16 @@ describe('DataQualityContext', () => { expect(mockHttpFetch).toBeCalledWith(path); }); - test('it should return the telemetry events', async () => { + test('it should return the telemetry events', () => { const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper }); - const telemetryEvents = await result.current.telemetryEvents; + const telemetryEvents = result.current.telemetryEvents; expect(telemetryEvents).toEqual(mockTelemetryEvents); }); test('it should return the isILMAvailable param', async () => { const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper }); - const isILMAvailable = await result.current.isILMAvailable; + const isILMAvailable = result.current.isILMAvailable; expect(isILMAvailable).toEqual(true); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx index 57dd5230721a0..06620ecacd04d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx @@ -9,13 +9,38 @@ import React, { useMemo } from 'react'; import type { PropsWithChildren } from 'react'; import type { HttpHandler } from '@kbn/core-http-browser'; import type { IToasts } from '@kbn/core-notifications-browser'; +import { PartialTheme, Theme } from '@elastic/charts'; + +import { EuiComboBoxOptionOption } from '@elastic/eui'; import type { TelemetryEvents } from '../../types'; -interface DataQualityProviderProps { +export interface DataQualityProviderProps { httpFetch: HttpHandler; isILMAvailable: boolean; telemetryEvents: TelemetryEvents; toasts: IToasts; + addSuccessToast: (toast: { title: string }) => void; + baseTheme: Theme; + canUserCreateAndReadCases: () => boolean; + endDate?: string | null; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + isAssistantEnabled: boolean; + lastChecked: string; + openCreateCaseFlyout: ({ + comments, + headerContent, + }: { + comments: string[]; + headerContent?: React.ReactNode; + }) => void; + patterns: string[]; + setLastChecked: (lastChecked: string) => void; + startDate?: string | null; + theme?: PartialTheme; + ilmPhases: string[]; + selectedIlmPhaseOptions: EuiComboBoxOptionOption[]; + setSelectedIlmPhaseOptions: (options: EuiComboBoxOptionOption[]) => void; } const DataQualityContext = React.createContext(undefined); @@ -26,6 +51,22 @@ export const DataQualityProvider: React.FC { const value = useMemo( () => ({ @@ -33,8 +74,45 @@ export const DataQualityProvider: React.FC{children}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx index 5085db2a93e51..a09a65f32b46e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx @@ -8,7 +8,10 @@ import { act, render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; import { IndexToCheck } from '../../../types'; import { CheckStatus, EMPTY_LAST_CHECKED_DATE } from '.'; @@ -23,17 +26,16 @@ describe('CheckStatus', () => { describe('when `indexToCheck` is not null', () => { beforeEach(() => { render( - - - + + + + + ); }); @@ -65,17 +67,16 @@ describe('CheckStatus', () => { describe('when `indexToCheck` is null', () => { beforeEach(() => { render( - - - + + + + + ); }); @@ -102,17 +103,16 @@ describe('CheckStatus', () => { ]; render( - - - + + + + + ); expect(screen.getByTestId('errorsPopover')).toBeInTheDocument(); @@ -120,17 +120,16 @@ describe('CheckStatus', () => { test('it does NOT render the errors popover when errors have NOT occurred', () => { render( - - - + + + + + ); expect(screen.queryByTestId('errorsPopover')).not.toBeInTheDocument(); @@ -144,17 +143,16 @@ describe('CheckStatus', () => { const setLastChecked = jest.fn(); render( - - - + + + + + ); expect(setLastChecked).toBeCalledWith(date); @@ -167,17 +165,16 @@ describe('CheckStatus', () => { jest.setSystemTime(new Date(date)); const { rerender } = render( - - - + + + + + ); // re-render with an updated `lastChecked` @@ -188,17 +185,16 @@ describe('CheckStatus', () => { }); rerender( - - - + + + + + ); act(() => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx index 9245b0adee84c..a39c7bbf7cc7d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx @@ -12,28 +12,24 @@ import moment from 'moment'; import { ErrorsPopover } from '../errors_popover'; import * as i18n from '../../../translations'; import type { ErrorSummary, IndexToCheck } from '../../../types'; +import { useDataQualityContext } from '../../data_quality_context'; export const EMPTY_LAST_CHECKED_DATE = '--'; interface Props { - addSuccessToast: (toast: { title: string }) => void; checkAllIndiciesChecked: number; checkAllTotalIndiciesToCheck: number; errorSummary: ErrorSummary[]; indexToCheck: IndexToCheck | null; - lastChecked: string; - setLastChecked: (lastChecked: string) => void; } const CheckStatusComponent: React.FC = ({ - addSuccessToast, checkAllIndiciesChecked, checkAllTotalIndiciesToCheck, errorSummary, indexToCheck, - lastChecked, - setLastChecked, }) => { + const { addSuccessToast, lastChecked, setLastChecked } = useDataQualityContext(); const [formattedDate, setFormattedDate] = useState(EMPTY_LAST_CHECKED_DATE); useEffect(() => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx index 064ec92a1ca81..ecedacc646043 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx @@ -9,7 +9,7 @@ import userEvent from '@testing-library/user-event'; import { act, render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../mock/test_providers/test_providers'; import { ErrorsPopover } from '.'; const mockCopyToClipboard = jest.fn((value) => true); @@ -36,9 +36,9 @@ describe('ErrorsPopover', () => { test('it disables the view errors button when `errorSummary` is empty', () => { render( - + - + ); expect(screen.getByTestId('viewErrors')).toBeDisabled(); @@ -46,9 +46,9 @@ describe('ErrorsPopover', () => { test('it enables the view errors button when `errorSummary` is NOT empty', () => { render( - + - + ); expect(screen.getByTestId('viewErrors')).not.toBeDisabled(); @@ -61,9 +61,9 @@ describe('ErrorsPopover', () => { jest.resetAllMocks(); render( - + - + ); const viewErrorsButton = screen.getByTestId('viewErrors'); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx index 1954e92ae5fc7..4ef8e57bf74b9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx @@ -10,7 +10,7 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { getErrorsViewerTableColumns } from './helpers'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../mock/test_providers/test_providers'; import { ErrorSummary } from '../../../types'; const errorSummary: ErrorSummary[] = [ @@ -67,9 +67,9 @@ describe('helpers', () => { const indexNameRender = columns[1].render; render( - + {indexNameRender != null && indexNameRender(hasIndexName.indexName, hasIndexName)} - + ); }); @@ -88,9 +88,9 @@ describe('helpers', () => { const indexNameRender = columns[1].render; render( - + {indexNameRender != null && indexNameRender(noIndexName.indexName, noIndexName)} - + ); }); @@ -110,9 +110,9 @@ describe('helpers', () => { const indexNameRender = columns[2].render; render( - + {indexNameRender != null && indexNameRender(hasIndexName.error, hasIndexName)} - + ); expect(screen.getByTestId('error')).toHaveTextContent(hasIndexName.error); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx index a1b6346eb2b8d..0354f687cda24 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx @@ -8,7 +8,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../mock/test_providers/test_providers'; import { ERROR, INDEX, PATTERN } from './translations'; import { ErrorSummary } from '../../../types'; import { ErrorsViewer } from '.'; @@ -51,9 +51,9 @@ describe('ErrorsViewer', () => { expectedColumns.forEach(({ id, expected }, i) => { test(`it renders the expected '${id}' column header`, () => { render( - + - + ); expect(screen.getByTestId(`tableHeaderCell_${id}_${i}`)).toHaveTextContent(expected); @@ -62,9 +62,9 @@ describe('ErrorsViewer', () => { test(`it renders the expected the errors`, () => { render( - + - + ); expect( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.test.tsx new file mode 100644 index 0000000000000..d2d0a388c465d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { IlmPhaseFilter } from '.'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; +import { + COLD_DESCRIPTION, + FROZEN_DESCRIPTION, + HOT_DESCRIPTION, + INDEX_LIFECYCLE_MANAGEMENT_PHASES, + UNMANAGED_DESCRIPTION, + WARM_DESCRIPTION, +} from '../../../translations'; + +describe('IlmPhaseFilter', () => { + it('renders combobox with ilmPhase label and preselected hot, warm, unmanaged options', () => { + render( + + + + + + ); + expect(screen.getByTestId('selectIlmPhases')).toBeInTheDocument(); + expect(screen.getByLabelText('ILM phase')).toBeInTheDocument(); + expect(screen.getByText('hot')).toBeInTheDocument(); + expect(screen.getByText('warm')).toBeInTheDocument(); + expect(screen.getByText('unmanaged')).toBeInTheDocument(); + }); + + it('does not preselect disabled cold, frozen options', () => { + render( + + + + + + ); + expect(screen.queryByText('cold')).not.toBeInTheDocument(); + expect(screen.queryByText('frozen')).not.toBeInTheDocument(); + }); + + describe('when dropdown opened', () => { + it('shows remaining disabled options', () => { + render( + + + + + + ); + const searchInput = screen.getByTestId('comboBoxSearchInput'); + + userEvent.click(searchInput); + + expect(screen.getByTitle('frozen')).toHaveAttribute('role', 'option'); + expect(screen.getByTitle('frozen')).toBeDisabled(); + expect(screen.getByTitle('cold')).toHaveAttribute('role', 'option'); + expect(screen.getByTitle('cold')).toBeDisabled(); + }); + }); + + describe('when hovering over search input', () => { + it('shows a tooltip with the ilm check description', async () => { + render( + + + + + + ); + + userEvent.hover(screen.getByTestId('comboBoxSearchInput')); + + await waitFor(() => + expect(screen.getByRole('tooltip')).toHaveTextContent(INDEX_LIFECYCLE_MANAGEMENT_PHASES) + ); + }); + }); + + describe('when hovering over options in dropdown', () => { + describe.each([ + { option: 'hot', tooltipDescription: HOT_DESCRIPTION }, + { option: 'warm', tooltipDescription: WARM_DESCRIPTION }, + { option: 'unmanaged', tooltipDescription: UNMANAGED_DESCRIPTION }, + { + option: 'cold', + tooltipDescription: COLD_DESCRIPTION, + }, + { + option: 'frozen', + tooltipDescription: FROZEN_DESCRIPTION, + }, + ])('when hovering over $option option', ({ option, tooltipDescription }) => { + it(`shows a tooltip with the ${option} description`, async () => { + render( + + + + + + ); + + expect(screen.getByPlaceholderText('Select one or more ILM phases')).toBeInTheDocument(); + + const searchInput = screen.getByTestId('comboBoxSearchInput'); + userEvent.click(searchInput); + userEvent.hover(screen.getByText(option.toLowerCase()), undefined, { + skipPointerEventsCheck: true, + }); + + await waitFor(() => + expect(screen.getByRole('tooltip')).toHaveTextContent(tooltipDescription) + ); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.tsx new file mode 100644 index 0000000000000..d0411349662fd --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/index.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFormLabel, + EuiToolTip, + useGeneratedHtmlId, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; + +import { ilmPhaseOptionsStatic } from '../../../constants'; +import { getIlmPhaseDescription } from '../../../helpers'; +import { + ILM_PHASE, + INDEX_LIFECYCLE_MANAGEMENT_PHASES, + SELECT_ONE_OR_MORE_ILM_PHASES, +} from '../../../translations'; +import { useDataQualityContext } from '../../data_quality_context'; +import { StyledFormControlLayout, StyledOption, StyledOptionLabel } from './styles'; + +const renderOption = ( + option: EuiComboBoxOptionOption +): React.ReactNode => ( + + + {`${option.label}`} + {': '} + {getIlmPhaseDescription(option.label)} + + +); + +const IlmPhaseFilterComponent: React.FC = () => { + const { selectedIlmPhaseOptions, setSelectedIlmPhaseOptions } = useDataQualityContext(); + const labelInputId = useGeneratedHtmlId({ prefix: 'labelInput' }); + const ilmFormLabel = useMemo( + () => {ILM_PHASE}, + [labelInputId] + ); + + const handleSetSelectedOptions = useCallback( + (selectedOptions: EuiComboBoxOptionOption[]) => { + setSelectedIlmPhaseOptions(selectedOptions); + }, + [setSelectedIlmPhaseOptions] + ); + + return ( + + + + + + ); +}; + +IlmPhaseFilterComponent.displayName = 'IlmPhaseFilterComponent'; + +export const IlmPhaseFilter = React.memo(IlmPhaseFilterComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/styles.tsx new file mode 100644 index 0000000000000..f2ff23d441165 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/ilm_phase_filter/styles.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormControlLayout } from '@elastic/eui'; +import styled from 'styled-components'; + +export const StyledOption = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +`; + +export const StyledOptionLabel = styled.span` + font-weight: ${({ theme }) => theme.eui.euiFontWeightBold}; +`; + +export const StyledFormControlLayout = styled(EuiFormControlLayout)` + height: 42px; + + .euiFormControlLayout__childrenWrapper { + overflow: visible; + } +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.test.tsx index a8eba4981e7e1..6b8994e0d7919 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.test.tsx @@ -13,9 +13,12 @@ import { EMPTY_STAT } from '../../helpers'; import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import { PatternRollup } from '../../types'; -import { Props, DataQualitySummary } from '.'; +import { DataQualitySummary } from '.'; import { getTotalDocsCount, getTotalIncompatible, @@ -62,34 +65,33 @@ const totalIndices = getTotalIndices(patternRollups); const totalIndicesChecked = getTotalIndicesChecked(patternRollups); const totalSizeInBytes = getTotalSizeInBytes(patternRollups); -const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: jest.fn(), - formatBytes, - formatNumber, - ilmPhases, - lastChecked, - openCreateCaseFlyout: jest.fn(), - patternIndexNames, - patternRollups, - patterns, - setLastChecked: jest.fn(), - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - totalSizeInBytes, - onCheckCompleted: jest.fn(), -}; - describe('DataQualitySummary', () => { beforeEach(() => { jest.clearAllMocks(); render( - - - + + + + + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx index d3f9ad9d23303..eca684ce5c218 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx @@ -6,108 +6,75 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import styled from 'styled-components'; -import { getErrorSummaries } from '../../helpers'; import { StatsRollup } from '../pattern/pattern_summary/stats_rollup'; import { SummaryActions } from './summary_actions'; -import type { OnCheckCompleted, PatternRollup } from '../../types'; +import { IlmPhaseFilter } from './ilm_phase_filter'; +import { useDataQualityContext } from '../data_quality_context'; +import { useResultsRollupContext } from '../../contexts/results_rollup_context'; const MAX_SUMMARY_ACTIONS_CONTAINER_WIDTH = 400; const MIN_SUMMARY_ACTIONS_CONTAINER_WIDTH = 235; +const StyledFlexGroup = styled(EuiFlexGroup)` + min-height: calc(174px - ${({ theme }) => theme.eui.euiSizeL} * 2); +`; + +const StyledFlexItem = styled(EuiFlexItem)` + gap: ${({ theme }) => theme.eui.euiSizeL}; +`; + const SummaryActionsContainerFlexItem = styled(EuiFlexItem)` max-width: ${MAX_SUMMARY_ACTIONS_CONTAINER_WIDTH}px; min-width: ${MIN_SUMMARY_ACTIONS_CONTAINER_WIDTH}px; - padding-right: ${({ theme }) => theme.eui.euiSizeXL}; `; -export interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhases: string[]; - lastChecked: string; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; - patternIndexNames: Record; - patternRollups: Record; - patterns: string[]; - setLastChecked: (lastChecked: string) => void; - totalDocsCount: number | undefined; - totalIncompatible: number | undefined; - totalIndices: number | undefined; - totalIndicesChecked: number | undefined; - totalSizeInBytes: number | undefined; - onCheckCompleted: OnCheckCompleted; -} +const StyledIlmPhaseFilterContainer = styled.div` + width: 100%; + max-width: 432px; + align-self: flex-end; +`; + +const StyledRollupContainer = styled.div` + margin-top: auto; +`; -const DataQualitySummaryComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - formatBytes, - formatNumber, - ilmPhases, - lastChecked, - openCreateCaseFlyout, - patternIndexNames, - patternRollups, - patterns, - setLastChecked, - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - totalSizeInBytes, - onCheckCompleted, -}) => { - const errorSummary = useMemo(() => getErrorSummaries(patternRollups), [patternRollups]); +const DataQualitySummaryComponent: React.FC = () => { + const { isILMAvailable } = useDataQualityContext(); + const { totalIndices, totalDocsCount, totalIndicesChecked, totalIncompatible, totalSizeInBytes } = + useResultsRollupContext(); return ( - - + + - + - - - - + + {isILMAvailable && ( + + + + )} + + + + + ); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx deleted file mode 100644 index 02b04225a544e..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx +++ /dev/null @@ -1,104 +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 userEvent from '@testing-library/user-event'; -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../../mock/test_providers/test_providers'; -import { Props, Actions } from '.'; - -const mockCopyToClipboard = jest.fn((value) => true); -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - copyToClipboard: (value: string) => mockCopyToClipboard(value), - }; -}); - -const ilmPhases = ['hot', 'warm', 'unmanaged']; - -const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: () => true, - getMarkdownComments: () => [], - ilmPhases, - openCreateCaseFlyout: jest.fn(), -}; - -describe('Actions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('when the action buttons are clicked', () => { - beforeEach(() => { - render( - - - - ); - }); - - test('it invokes openCreateCaseFlyout when the add to new case button is clicked', () => { - const button = screen.getByTestId('addToNewCase'); - - userEvent.click(button); - - expect(defaultProps.openCreateCaseFlyout).toBeCalled(); - }); - - test('it invokes addSuccessToast when the copy to clipboard button is clicked', () => { - const button = screen.getByTestId('copyToClipboard'); - - userEvent.click(button); - - expect(defaultProps.addSuccessToast).toBeCalledWith({ - title: 'Copied results to the clipboard', - }); - }); - }); - - test('it disables the add to new case button when the user cannot create cases', () => { - const canUserCreateAndReadCases = () => false; - - render( - - - - ); - - const button = screen.getByTestId('addToNewCase'); - - expect(button).toBeDisabled(); - }); - - test('it disables the add to new case button when `ilmPhases` is empty', () => { - render( - - - - ); - - const button = screen.getByTestId('addToNewCase'); - - expect(button).toBeDisabled(); - }); - - test('it disables the copy to clipboard button when `ilmPhases` is empty', () => { - render( - - - - ); - - const button = screen.getByTestId('copyToClipboard'); - - expect(button).toBeDisabled(); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx deleted file mode 100644 index 549f420c3fa9d..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { copyToClipboard, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { - ADD_TO_NEW_CASE, - COPIED_RESULTS_TOAST_TITLE, - COPY_TO_CLIPBOARD, -} from '../../../../translations'; -import { useAddToNewCase } from '../../../../use_add_to_new_case'; - -export interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - getMarkdownComments: () => string[]; - ilmPhases: string[]; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; -} - -const ActionsComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - getMarkdownComments, - ilmPhases, - openCreateCaseFlyout, -}) => { - const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({ - canUserCreateAndReadCases, - openCreateCaseFlyout, - }); - - const onClickAddToCase = useCallback( - () => onAddToNewCase([getMarkdownComments().join('\n')]), - [getMarkdownComments, onAddToNewCase] - ); - - const onCopy = useCallback(() => { - const markdown = getMarkdownComments().join('\n'); - copyToClipboard(markdown); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, getMarkdownComments]); - - const addToNewCaseContextMenuOnClick = useCallback(() => { - onClickAddToCase(); - }, [onClickAddToCase]); - - const disableAll = ilmPhases.length === 0; - - return ( - - - - {ADD_TO_NEW_CASE} - - - - - - {COPY_TO_CLIPBOARD} - - - - ); -}; - -ActionsComponent.displayName = 'ActionsComponent'; - -export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts deleted file mode 100644 index 70d522f18177a..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts +++ /dev/null @@ -1,317 +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 { checkIndex, EMPTY_PARTITIONED_FIELD_METADATA } from './check_index'; -import { EMPTY_STAT } from '../../../../helpers'; -import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response'; -import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values'; -import { UnallowedValueRequestItem } from '../../../../types'; -import { EcsFlatTyped } from '../../../../constants'; - -let mockFetchMappings = jest.fn( - ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => - new Promise((resolve) => { - resolve(mockMappingsResponse); // happy path - }) -); - -jest.mock('../../../../use_mappings/helpers', () => ({ - fetchMappings: ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => - mockFetchMappings({ - abortController, - patternOrIndexName, - }), -})); - -const mockFetchUnallowedValues = jest.fn( - ({ - abortController, - indexName, - requestItems, - }: { - abortController: AbortController; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) -); - -jest.mock('../../../../use_unallowed_values/helpers', () => { - const original = jest.requireActual('../../../../use_unallowed_values/helpers'); - - return { - ...original, - fetchUnallowedValues: ({ - abortController, - indexName, - requestItems, - }: { - abortController: AbortController; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => - mockFetchUnallowedValues({ - abortController, - indexName, - requestItems, - }), - }; -}); - -describe('checkIndex', () => { - 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'; - const pattern = 'auditbeat-*'; - const httpFetch = jest.fn(); - - describe('when `checkIndex` successfully completes the check', () => { - const onCheckCompleted = jest.fn(); - - beforeEach(async () => { - jest.clearAllMocks(); - - await checkIndex({ - abortController: new AbortController(), - batchId: 'batch-id', - checkAllStartTime: Date.now(), - ecsMetadata: EcsFlatTyped, - formatBytes, - formatNumber, - httpFetch, - indexName, - isLastCheck: false, - onCheckCompleted, - pattern, - version: EcsVersion, - }); - }); - - test('it invokes onCheckCompleted with a null `error`', () => { - expect(onCheckCompleted.mock.calls[0][0].error).toBeNull(); - }); - - test('it invokes onCheckCompleted with the expected `indexName`', () => { - expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); - }); - - test('it invokes onCheckCompleted with the non-default `partitionedFieldMetadata`', () => { - expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).not.toEqual( - EMPTY_PARTITIONED_FIELD_METADATA - ); - }); - - test('it invokes onCheckCompleted with the expected`pattern`', () => { - expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); - }); - - test('it invokes onCheckCompleted with the expected `version`', () => { - expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); - }); - }); - - describe('happy path, when the signal is aborted', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('it does NOT invoke onCheckCompleted', async () => { - const onCheckCompleted = jest.fn(); - - const abortController = new AbortController(); - abortController.abort(); - - await checkIndex({ - abortController, - batchId: 'batch-id', - checkAllStartTime: Date.now(), - ecsMetadata: EcsFlatTyped, - formatBytes, - formatNumber, - httpFetch, - indexName, - isLastCheck: false, - onCheckCompleted, - pattern, - version: EcsVersion, - }); - - expect(onCheckCompleted).not.toBeCalled(); - }); - }); - - describe('when an error occurs', () => { - const onCheckCompleted = jest.fn(); - const error = 'simulated fetch mappings error'; - - beforeEach(async () => { - jest.clearAllMocks(); - - mockFetchMappings = jest.fn( - ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => new Promise((_, reject) => reject(new Error(error))) - ); - - await checkIndex({ - abortController: new AbortController(), - batchId: 'batch-id', - checkAllStartTime: Date.now(), - ecsMetadata: EcsFlatTyped, - formatBytes, - formatNumber, - httpFetch, - indexName, - isLastCheck: false, - onCheckCompleted, - pattern, - version: EcsVersion, - }); - }); - - test('it invokes onCheckCompleted with the expected `error`', () => { - expect(onCheckCompleted.mock.calls[0][0].error).toEqual(error); - }); - - test('it invokes onCheckCompleted with the expected `indexName`', () => { - expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); - }); - - test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { - expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); - }); - - test('it invokes onCheckCompleted with the expected `pattern`', () => { - expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); - }); - - test('it invokes onCheckCompleted with the expected `version`', () => { - expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); - }); - }); - - describe('when an error occurs, but the error does not have a toString', () => { - const onCheckCompleted = jest.fn(); - - beforeEach(async () => { - jest.clearAllMocks(); - - mockFetchMappings = jest.fn( - ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - // eslint-disable-next-line prefer-promise-reject-errors - }) => new Promise((_, reject) => reject(undefined)) - ); - - await checkIndex({ - abortController: new AbortController(), - batchId: 'batch-id', - checkAllStartTime: Date.now(), - ecsMetadata: EcsFlatTyped, - formatBytes, - formatNumber, - httpFetch, - indexName, - isLastCheck: false, - onCheckCompleted, - pattern, - version: EcsVersion, - }); - }); - - test('it invokes onCheckCompleted with the fallback `error`', () => { - expect(onCheckCompleted.mock.calls[0][0].error).toEqual( - `An error occurred checking index ${indexName}` - ); - }); - - test('it invokes onCheckCompleted with the expected `indexName`', () => { - expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); - }); - - test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { - expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); - }); - - test('it invokes onCheckCompleted with the expected `pattern`', () => { - expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); - }); - - test('it invokes onCheckCompleted with the expected `version`', () => { - expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); - }); - }); - - describe('when an error occurs, and the signal is aborted', () => { - const onCheckCompleted = jest.fn(); - const abortController = new AbortController(); - abortController.abort(); - - const error = 'simulated fetch mappings error'; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('it does NOT invoke onCheckCompleted', async () => { - mockFetchMappings = jest.fn( - ({ - // eslint-disable-next-line @typescript-eslint/no-shadow - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => new Promise((_, reject) => reject(new Error(error))) - ); - - await checkIndex({ - abortController, - batchId: 'batch-id', - checkAllStartTime: Date.now(), - ecsMetadata: EcsFlatTyped, - formatBytes, - formatNumber, - httpFetch, - indexName, - isLastCheck: false, - onCheckCompleted, - pattern, - version: EcsVersion, - }); - - expect(onCheckCompleted).not.toBeCalled(); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx index fbf0be3354a53..97e93124b84cf 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx @@ -11,14 +11,13 @@ import { act, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../mock/test_providers/test_providers'; import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values'; import { CANCEL, CHECK_ALL } from '../../../../translations'; -import { - OnCheckCompleted, - PartitionedFieldMetadata, - UnallowedValueRequestItem, -} from '../../../../types'; +import { OnCheckCompleted, UnallowedValueRequestItem } from '../../../../types'; import { CheckAll } from '.'; import { EMPTY_STAT } from '../../../../helpers'; @@ -30,64 +29,29 @@ const defaultNumberFormat = '0,0.[000]'; const mockFormatNumber = (value: number | undefined) => value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; -const mockFetchMappings = jest.fn( - ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => - new Promise((resolve) => { - resolve(mockMappingsResponse); // happy path - }) +const mockFetchMappings = jest.fn(() => + Promise.resolve( + mockMappingsResponse // happy path + ) ); jest.mock('../../../../use_mappings/helpers', () => ({ - fetchMappings: ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => - mockFetchMappings({ - abortController, - patternOrIndexName, - }), + fetchMappings: (_: { abortController: AbortController; patternOrIndexName: string }) => + mockFetchMappings(), })); -const mockFetchUnallowedValues = jest.fn( - ({ - abortController, - indexName, - requestItems, - }: { - abortController: AbortController; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) -); +const mockFetchUnallowedValues = jest.fn(() => Promise.resolve(mockUnallowedValuesResponse)); jest.mock('../../../../use_unallowed_values/helpers', () => { const original = jest.requireActual('../../../../use_unallowed_values/helpers'); return { ...original, - fetchUnallowedValues: ({ - abortController, - indexName, - requestItems, - }: { + fetchUnallowedValues: (_: { abortController: AbortController; indexName: string; requestItems: UnallowedValueRequestItem[]; - }) => - mockFetchUnallowedValues({ - abortController, - indexName, - requestItems, - }), + }) => mockFetchUnallowedValues(), }; }); @@ -120,20 +84,26 @@ describe('CheckAll', () => { test('it renders the expected button text when a check is NOT running', () => { render( - - - + + + + + ); expect(screen.getByTestId('checkAll')).toHaveTextContent(CHECK_ALL); @@ -141,20 +111,26 @@ describe('CheckAll', () => { test('it renders a disabled button when ILM available and ilmPhases is an empty array', () => { render( - - - + + + + + ); expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeTruthy(); @@ -162,20 +138,27 @@ describe('CheckAll', () => { test('it renders the expected button when ILM is NOT available', () => { render( - - - + + + + + ); expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeFalsy(); @@ -183,20 +166,27 @@ describe('CheckAll', () => { test('it renders the expected button text when a check is running', () => { render( - - - + + + + + ); const button = screen.getByTestId('checkAll'); @@ -211,40 +201,35 @@ describe('CheckAll', () => { /** stores the result of invoking `CheckAll`'s `formatNumber` function */ let formatNumberResult = ''; - const onCheckCompleted: OnCheckCompleted = jest.fn( - ({ - formatBytes, - formatNumber, - }: { - error: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata | null; - pattern: string; - version: string; - }) => { - const value = 123456789; // numeric input to `CheckAll`'s `formatNumber` function - - formatNumberResult = formatNumber(value); - } - ); + const onCheckCompleted: OnCheckCompleted = jest.fn(({ formatBytes, formatNumber }) => { + const value = 123456789; // numeric input to `CheckAll`'s `formatNumber` function + + formatNumberResult = formatNumber(value); + }); render( - - - + + + + + ); const button = screen.getByTestId('checkAll'); @@ -260,40 +245,34 @@ describe('CheckAll', () => { /** stores the result of invoking `CheckAll`'s `formatNumber` function */ let formatNumberResult = ''; - const onCheckCompleted: OnCheckCompleted = jest.fn( - ({ - formatBytes, - formatNumber, - }: { - error: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata | null; - pattern: string; - version: string; - }) => { - const value = undefined; // undefined input to `CheckAll`'s `formatNumber` function - - formatNumberResult = formatNumber(value); - } - ); + const onCheckCompleted: OnCheckCompleted = jest.fn(({ formatBytes, formatNumber }) => { + const value = undefined; // undefined input to `CheckAll`'s `formatNumber` function + formatNumberResult = formatNumber(value); + }); render( - - - + + + + + ); const button = screen.getByTestId('checkAll'); @@ -314,20 +293,27 @@ describe('CheckAll', () => { jest.clearAllMocks(); render( - - - + + + + + ); const button = screen.getByTestId('checkAll'); @@ -358,20 +344,28 @@ describe('CheckAll', () => { jest.useFakeTimers(); render( - - - + + + + + ); const button = screen.getByTestId('checkAll'); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/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/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx index 5155c2a07f9cf..c8bb58de2ef44 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/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/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx @@ -5,19 +5,17 @@ * 2.0. */ -import { EcsVersion } from '@elastic/ecs'; - import { EuiButton } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; -import { checkIndex } from './check_index'; +import { useResultsRollupContext } from '../../../../contexts/results_rollup_context'; +import { checkIndex } from '../../../../utils/check_index'; import { useDataQualityContext } from '../../../data_quality_context'; import { getAllIndicesToCheck } from './helpers'; import * as i18n from '../../../../translations'; -import type { IndexToCheck, OnCheckCompleted } from '../../../../types'; -import { EcsFlatTyped } from '../../../../constants'; +import type { IndexToCheck } from '../../../../types'; const CheckAllButton = styled(EuiButton)` width: 112px; @@ -35,13 +33,7 @@ async function wait(ms: number) { } interface Props { - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhases: string[]; incrementCheckAllIndiciesChecked: () => void; - onCheckCompleted: OnCheckCompleted; - patternIndexNames: Record; - patterns: string[]; setCheckAllIndiciesChecked: (checkAllIndiciesChecked: number) => void; setCheckAllTotalIndiciesToCheck: (checkAllTotalIndiciesToCheck: number) => void; setIndexToCheck: (indexToCheck: IndexToCheck | null) => void; @@ -50,18 +42,14 @@ interface Props { const DELAY_AFTER_EVERY_CHECK_COMPLETES = 3000; // ms const CheckAllComponent: React.FC = ({ - formatBytes, - formatNumber, - ilmPhases, incrementCheckAllIndiciesChecked, - onCheckCompleted, - patternIndexNames, - patterns, setCheckAllIndiciesChecked, setCheckAllTotalIndiciesToCheck, setIndexToCheck, }) => { - const { httpFetch, isILMAvailable } = useDataQualityContext(); + const { httpFetch, isILMAvailable, formatBytes, formatNumber, ilmPhases, patterns } = + useDataQualityContext(); + const { onCheckCompleted, patternIndexNames } = useResultsRollupContext(); const abortController = useRef(new AbortController()); const [isRunning, setIsRunning] = useState(false); @@ -98,16 +86,15 @@ const CheckAllComponent: React.FC = ({ abortController: abortController.current, batchId, checkAllStartTime: startTime, - ecsMetadata: EcsFlatTyped, formatBytes, formatNumber, + isCheckAll: true, httpFetch, indexName, isLastCheck: allIndicesToCheck.length > 0 ? checked === allIndicesToCheck.length - 1 : true, onCheckCompleted, pattern, - version: EcsVersion, }); if (!abortController.current.signal.aborted) { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx index 7d139121afbc4..d202bdf9b342d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx @@ -14,9 +14,12 @@ import { EMPTY_STAT } from '../../../helpers'; import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../mock/test_providers/test_providers'; import { PatternRollup } from '../../../types'; -import { Props, SummaryActions } from '.'; +import { SummaryActions } from '.'; import { getTotalDocsCount, getTotalIncompatible, @@ -64,6 +67,8 @@ const patternIndexNames: Record = { ], }; +const addSuccessToast = jest.fn(); + const lastChecked = '2023-03-28T23:27:28.159Z'; const totalDocsCount = getTotalDocsCount(patternRollups); @@ -72,35 +77,34 @@ const totalIndices = getTotalIndices(patternRollups); const totalIndicesChecked = getTotalIndicesChecked(patternRollups); const totalSizeInBytes = getTotalSizeInBytes(patternRollups); -const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: () => true, - errorSummary: [], - formatBytes, - formatNumber, - ilmPhases, - lastChecked, - openCreateCaseFlyout: jest.fn(), - onCheckCompleted: jest.fn(), - patternIndexNames, - patternRollups, - patterns, - setLastChecked: jest.fn(), - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - sizeInBytes: totalSizeInBytes, -}; - describe('SummaryActions', () => { beforeEach(() => { jest.clearAllMocks(); render( - - - + + + + + ); }); @@ -121,7 +125,7 @@ describe('SummaryActions', () => { userEvent.click(button); - expect(defaultProps.addSuccessToast).toBeCalledWith({ + expect(addSuccessToast).toBeCalledWith({ title: 'Copied results to the clipboard', }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx index ed9aeaa8f59d8..e0d7233f538d2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx @@ -7,7 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { sortBy } from 'lodash/fp'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; import { CheckAll } from './check_all'; import { CheckStatus } from '../check_status'; @@ -22,16 +23,16 @@ import { getSummaryTableMarkdownRow, } from '../../index_properties/markdown/helpers'; import { defaultSort, getSummaryTableItems } from '../../pattern/helpers'; -import { Actions } from './actions'; -import type { - DataQualityCheckResult, - ErrorSummary, - IndexToCheck, - OnCheckCompleted, - PatternRollup, -} from '../../../types'; -import { getSizeInBytes } from '../../../helpers'; +import type { DataQualityCheckResult, IndexToCheck, PatternRollup } from '../../../types'; +import { getErrorSummaries, getSizeInBytes } from '../../../helpers'; import { useDataQualityContext } from '../../data_quality_context'; +import { useResultsRollupContext } from '../../../contexts/results_rollup_context'; +import { Actions } from '../../actions'; + +const StyledActionsContainerFlexItem = styled(EuiFlexItem)` + margin-top: auto; + padding-bottom: 3px; +`; export const getResultsSortedByDocsCount = ( results: Record | undefined @@ -135,54 +136,18 @@ export const getAllMarkdownComments = ({ ); }; -export interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - errorSummary: ErrorSummary[]; - ilmPhases: string[]; - lastChecked: string; - onCheckCompleted: OnCheckCompleted; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; - patternIndexNames: Record; - patternRollups: Record; - patterns: string[]; - setLastChecked: (lastChecked: string) => void; - totalDocsCount: number | undefined; - totalIncompatible: number | undefined; - totalIndices: number | undefined; - totalIndicesChecked: number | undefined; - sizeInBytes: number | undefined; -} - -const SummaryActionsComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - formatBytes, - formatNumber, - errorSummary, - ilmPhases, - lastChecked, - onCheckCompleted, - openCreateCaseFlyout, - patternIndexNames, - patternRollups, - patterns, - setLastChecked, - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - sizeInBytes, -}) => { - const { isILMAvailable } = useDataQualityContext(); +const SummaryActionsComponent: React.FC = () => { + const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); + const { + patternRollups, + totalIndices, + totalDocsCount, + totalIndicesChecked, + totalIncompatible, + patternIndexNames, + totalSizeInBytes, + } = useResultsRollupContext(); + const errorSummary = useMemo(() => getErrorSummaries(patternRollups), [patternRollups]); const [indexToCheck, setIndexToCheck] = useState(null); const [checkAllIndiciesChecked, setCheckAllIndiciesChecked] = useState(0); const [checkAllTotalIndiciesToCheck, setCheckAllTotalIndiciesToCheck] = useState(0); @@ -190,31 +155,32 @@ const SummaryActionsComponent: React.FC = ({ setCheckAllIndiciesChecked((current) => current + 1); }, []); - const getMarkdownComments = useCallback( - (): string[] => [ - getDataQualitySummaryMarkdownComment({ - formatBytes, - formatNumber, - totalDocsCount, - totalIncompatible, - totalIndices, - totalIndicesChecked, - sizeInBytes, - }), - ...getAllMarkdownComments({ - formatBytes, - formatNumber, - isILMAvailable, - patternIndexNames, - patternRollups, - }), - getErrorsMarkdownTable({ - errorSummary, - getMarkdownTableRows: getErrorsMarkdownTableRows, - headerNames: [PATTERN, INDEX, ERROR], - title: ERRORS, - }), - ], + const markdownComment = useMemo( + () => + [ + getDataQualitySummaryMarkdownComment({ + formatBytes, + formatNumber, + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + sizeInBytes: totalSizeInBytes, + }), + ...getAllMarkdownComments({ + formatBytes, + formatNumber, + isILMAvailable, + patternIndexNames, + patternRollups, + }), + getErrorsMarkdownTable({ + errorSummary, + getMarkdownTableRows: getErrorsMarkdownTableRows, + headerNames: [PATTERN, INDEX, ERROR], + title: ERRORS, + }), + ].join('\n'), [ errorSummary, formatBytes, @@ -222,11 +188,11 @@ const SummaryActionsComponent: React.FC = ({ isILMAvailable, patternIndexNames, patternRollups, - sizeInBytes, totalDocsCount, totalIncompatible, totalIndices, totalIndicesChecked, + totalSizeInBytes, ] ); @@ -235,13 +201,7 @@ const SummaryActionsComponent: React.FC = ({ = ({ - + - + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx index f57d9f52737d7..ee3241cd74d8b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx @@ -8,7 +8,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { ErrorEmptyPrompt } from '.'; describe('ErrorEmptyPrompt', () => { @@ -16,9 +16,9 @@ describe('ErrorEmptyPrompt', () => { const title = 'This is the title of this work'; render( - + - + ); expect(screen.getByTestId('errorEmptyPrompt').textContent?.includes(title)).toBe(true); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx index 6efe7579d7325..23031b9210df1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx @@ -13,7 +13,7 @@ import { import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { IlmPhaseCounts } from '.'; import { getIlmExplainPhaseCounts } from '../pattern/helpers'; @@ -74,9 +74,9 @@ const pattern = 'packetbeat-*'; describe('IlmPhaseCounts', () => { test('it renders the expected counts', () => { render( - + - + ); expect(screen.getByTestId('ilmPhaseCounts')).toHaveTextContent( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx index 1c37ec799c53c..19e6c47a2c365 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx @@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { EmptyPromptBody } from './empty_prompt_body'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; describe('EmptyPromptBody', () => { const content = 'foo bar baz @baz'; test('it renders the expected content', () => { render( - + - + ); expect(screen.getByTestId('emptyPromptBody')).toHaveTextContent(content); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx index 6bb3b72ed3ece..760d16f8a87a5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx @@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { EmptyPromptTitle } from './empty_prompt_title'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; describe('EmptyPromptTitle', () => { const title = 'What is a great title?'; test('it renders the expected content', () => { render( - + - + ); expect(screen.getByTestId('emptyPromptTitle')).toHaveTextContent(title); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.ts index e47d18685615f..c0046da0ea814 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.ts @@ -25,7 +25,6 @@ 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 SUMMARY_TAB_ID = 'summaryTab'; export const EMPTY_METADATA: PartitionedFieldMetadata = { all: [], diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx index 973f5dde95e45..4e991d7878ace 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; import numeral from '@elastic/numeral'; -import { HttpHandler } from '@kbn/core-http-browser'; import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { EMPTY_STAT } from '../../helpers'; -import { mockMappingsResponse } from '../../mock/mappings_response/mock_mappings_response'; import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; -import { TestProviders } from '../../mock/test_providers/test_providers'; -import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_unallowed_values'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import { LOADING_MAPPINGS, LOADING_UNALLOWED_VALUES } from './translations'; -import { UnallowedValueRequestItem } from '../../types'; import { IndexProperties, Props } from '.'; +import { getCheckState } from '../../stub/get_check_state'; +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; @@ -31,101 +31,32 @@ const formatNumber = (value: number | undefined) => const pattern = 'auditbeat-*'; const patternRollup = auditbeatWithAllResults; -const mockFetchMappings = jest.fn( - ({ - abortController, - httpFetch, - patternOrIndexName, - }: { - abortController: AbortController; - httpFetch: HttpHandler; - patternOrIndexName: string; - }) => - new Promise((resolve) => { - resolve(mockMappingsResponse); // happy path - }) -); - -jest.mock('../../use_mappings/helpers', () => ({ - fetchMappings: ({ - abortController, - httpFetch, - patternOrIndexName, - }: { - abortController: AbortController; - httpFetch: HttpHandler; - patternOrIndexName: string; - }) => - mockFetchMappings({ - abortController, - httpFetch, - patternOrIndexName, - }), -})); - -const mockFetchUnallowedValues = jest.fn( - ({ - abortController, - httpFetch, - indexName, - requestItems, - }: { - abortController: AbortController; - httpFetch: HttpHandler; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) -); - -jest.mock('../../use_unallowed_values/helpers', () => { - const original = jest.requireActual('../../use_unallowed_values/helpers'); - - return { - ...original, - fetchUnallowedValues: ({ - abortController, - httpFetch, - indexName, - requestItems, - }: { - abortController: AbortController; - httpFetch: HttpHandler; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => - mockFetchUnallowedValues({ - abortController, - httpFetch, - indexName, - requestItems, - }), - }; -}); - const defaultProps: Props = { - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: jest.fn(), docsCount: auditbeatWithAllResults.docsCount ?? 0, - formatBytes, - formatNumber, - getGroupByFieldsOnClick: jest.fn(), - indexId: '1xxx', ilmPhase: 'hot', indexName: 'auditbeat-custom-index-1', - isAssistantEnabled: true, - openCreateCaseFlyout: jest.fn(), pattern, patternRollup, - baseTheme: DARK_THEME, - updatePatternRollup: jest.fn(), }; describe('IndexProperties', () => { test('it renders the tab content', async () => { render( - - - + + + + + ); await waitFor(() => { @@ -144,21 +75,24 @@ describe('IndexProperties', () => { }); test('it displays the expected empty prompt content', async () => { - mockFetchMappings.mockImplementation( - ({ - // eslint-disable-next-line @typescript-eslint/no-shadow - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => new Promise((_, reject) => reject(new Error(error))) - ); - render( - - - + + + + + ); await waitFor(() => { @@ -182,23 +116,24 @@ describe('IndexProperties', () => { }); test('it displays the expected empty prompt content', async () => { - mockFetchUnallowedValues.mockImplementation( - ({ - // eslint-disable-next-line @typescript-eslint/no-shadow - abortController, - indexName, - requestItems, - }: { - abortController: AbortController; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => new Promise((_, reject) => reject(new Error(error))) - ); - render( - - - + + + + + ); await waitFor(() => { @@ -217,20 +152,24 @@ describe('IndexProperties', () => { }); test('it displays the expected loading prompt content', async () => { - mockFetchMappings.mockImplementation( - ({ - abortController, - patternOrIndexName, - }: { - abortController: AbortController; - patternOrIndexName: string; - }) => new Promise(() => {}) // <-- will never resolve or reject - ); - render( - - - + + + + + ); await waitFor(() => { @@ -247,22 +186,24 @@ describe('IndexProperties', () => { }); test('it displays the expected loading prompt content', async () => { - mockFetchUnallowedValues.mockImplementation( - ({ - abortController, - indexName, - requestItems, - }: { - abortController: AbortController; - indexName: string; - requestItems: UnallowedValueRequestItem[]; - }) => new Promise(() => {}) // <-- will never resolve or reject - ); - render( - - - + + + + + ); await waitFor(() => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx index a9dce4dafc741..f7b1bb37e36f1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx @@ -5,350 +5,83 @@ * 2.0. */ -import { EcsVersion } from '@elastic/ecs'; -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; +import React from 'react'; -import { getUnallowedValueRequestItems } from '../allowed_values/helpers'; +import { EuiSpacer } from '@elastic/eui'; import { ErrorEmptyPrompt } from '../error_empty_prompt'; -import { - EMPTY_METADATA, - getMappingsProperties, - getSortedPartitionedFieldMetadata, - hasAllDataFetchingCompleted, - INCOMPATIBLE_TAB_ID, -} from './helpers'; import { LoadingEmptyPrompt } from '../loading_empty_prompt'; import { getIndexPropertiesContainerId } from '../pattern/helpers'; -import { getTabs } from '../tabs/helpers'; -import { - getAllIncompatibleMarkdownComments, - getIncompatibleValuesFields, - getIncompatibleMappingsFields, - getSameFamilyFields, -} from '../tabs/incompatible_tab/helpers'; import * as i18n from './translations'; -import type { IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../../types'; -import { useAddToNewCase } from '../../use_add_to_new_case'; -import { useMappings } from '../../use_mappings'; -import { useUnallowedValues } from '../../use_unallowed_values'; +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 { formatStorageResult, postStorageResult, getSizeInBytes } from '../../helpers'; -import { EcsFlatTyped } from '../../constants'; - -const EMPTY_MARKDOWN_COMMENTS: string[] = []; export interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; docsCount: number; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; ilmPhase: IlmPhase | undefined; - indexId: string | null | undefined; indexName: string; - isAssistantEnabled: boolean; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; pattern: string; patternRollup: PatternRollup | undefined; - theme?: PartialTheme; - baseTheme: Theme; - updatePatternRollup: (patternRollup: PatternRollup) => void; + sizeInBytes?: number; } const IndexPropertiesComponent: React.FC = ({ - addSuccessToast, - baseTheme, - canUserCreateAndReadCases, docsCount, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, ilmPhase, - indexId, indexName, - isAssistantEnabled, - openCreateCaseFlyout, pattern, patternRollup, - theme, - updatePatternRollup, + sizeInBytes, }) => { - const { error: mappingsError, indexes, loading: loadingMappings } = useMappings(indexName); - const { telemetryEvents, isILMAvailable, httpFetch, toasts } = useDataQualityContext(); - - const requestItems = useMemo( - () => - getUnallowedValueRequestItems({ - ecsMetadata: EcsFlatTyped, - indexName, - }), - [indexName] - ); - - const { - error: unallowedValuesError, - loading: loadingUnallowedValues, - unallowedValues, - requestTime, - } = useUnallowedValues({ indexName, requestItems }); - - const mappingsProperties = useMemo( - () => - getMappingsProperties({ - indexes, - indexName, - }), - [indexName, indexes] - ); - - const partitionedFieldMetadata: PartitionedFieldMetadata | null = useMemo( - () => - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings, - mappingsProperties, - unallowedValues, - }), - [loadingMappings, mappingsProperties, unallowedValues] - ); - - const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({ - canUserCreateAndReadCases, - indexName, - openCreateCaseFlyout, - }); - - const [selectedTabId, setSelectedTabId] = useState(INCOMPATIBLE_TAB_ID); - - const tabs = useMemo( - () => - getTabs({ - addSuccessToast, - addToNewCaseDisabled, - formatBytes, - formatNumber, - docsCount, - getGroupByFieldsOnClick, - ilmPhase, - isAssistantEnabled, - indexName, - onAddToNewCase, - partitionedFieldMetadata: partitionedFieldMetadata ?? EMPTY_METADATA, - pattern, - patternDocsCount: patternRollup?.docsCount ?? 0, - setSelectedTabId, - stats: patternRollup?.stats ?? null, - theme, - baseTheme, - }), - [ - addSuccessToast, - addToNewCaseDisabled, - docsCount, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, - ilmPhase, - indexName, - isAssistantEnabled, - onAddToNewCase, - partitionedFieldMetadata, - pattern, - patternRollup?.docsCount, - patternRollup?.stats, - theme, - baseTheme, - ] - ); - - const onSelectedTabChanged = useCallback((id: string) => { - setSelectedTabId(id); - }, []); - - const selectedTabContent = useMemo( - () => ( - <> - - {tabs.find((obj) => obj.id === selectedTabId)?.content} - - ), - [selectedTabId, tabs] - ); - - const renderTabs = useCallback( - () => - tabs.map((tab, index) => ( - onSelectedTabChanged(tab.id)} - > - {tab.name} - - )), - [onSelectedTabChanged, selectedTabId, tabs] - ); - - useEffect(() => { - if (hasAllDataFetchingCompleted({ loadingMappings, loadingUnallowedValues })) { - const error: string | null = mappingsError ?? unallowedValuesError; - const indexIncompatible: number | undefined = - error == null && partitionedFieldMetadata != null - ? partitionedFieldMetadata.incompatible.length - : undefined; - - const indexSameFamily: number | undefined = - error == null && partitionedFieldMetadata != null - ? partitionedFieldMetadata.sameFamily.length - : undefined; - - if (patternRollup != null) { - const markdownComments = - partitionedFieldMetadata != null - ? getAllIncompatibleMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount: patternRollup.docsCount ?? 0, - sizeInBytes: patternRollup.sizeInBytes, - }) - : EMPTY_MARKDOWN_COMMENTS; - - const checkedAt = partitionedFieldMetadata ? Date.now() : undefined; - - const updatedRollup = { - ...patternRollup, - results: { - ...patternRollup.results, - [indexName]: { - docsCount, - error, - ilmPhase, - incompatible: indexIncompatible, - indexName, - markdownComments, - pattern, - sameFamily: indexSameFamily, - checkedAt, - }, - }, - }; - updatePatternRollup(updatedRollup); - - if (indexName && requestTime != null && requestTime > 0 && partitionedFieldMetadata) { - const report = { - batchId: uuidv4(), - ecsVersion: EcsVersion, - errorCount: error ? 1 : 0, - ilmPhase, - indexId, - indexName, - isCheckAll: false, - numberOfDocuments: docsCount, - numberOfFields: partitionedFieldMetadata.all.length, - numberOfIncompatibleFields: indexIncompatible, - numberOfEcsFields: partitionedFieldMetadata.ecsCompliant.length, - numberOfCustomFields: partitionedFieldMetadata.custom.length, - numberOfIndices: 1, - numberOfIndicesChecked: 1, - numberOfSameFamily: indexSameFamily, - sizeInBytes: getSizeInBytes({ stats: patternRollup.stats, indexName }), - timeConsumedMs: requestTime, - sameFamilyFields: getSameFamilyFields(partitionedFieldMetadata.sameFamily), - unallowedMappingFields: getIncompatibleMappingsFields( - partitionedFieldMetadata.incompatible - ), - unallowedValueFields: getIncompatibleValuesFields( - partitionedFieldMetadata.incompatible - ), - }; - telemetryEvents.reportDataQualityIndexChecked?.(report); - - const result = updatedRollup.results[indexName]; - if (result) { - const storageResult = formatStorageResult({ result, report, partitionedFieldMetadata }); - postStorageResult({ storageResult, httpFetch, toasts }); - } - } - } - } - }, [ - docsCount, - formatBytes, - formatNumber, - httpFetch, - ilmPhase, - indexId, - indexName, - isILMAvailable, - loadingMappings, - loadingUnallowedValues, - mappingsError, - partitionedFieldMetadata, - pattern, - patternRollup, - requestTime, - telemetryEvents, - toasts, - unallowedValuesError, - updatePatternRollup, - ]); + 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 (loadingMappings) { + if (isLoadingMappings) { return ; - } else if (loadingUnallowedValues) { + } else if (isLoadingUnallowedValues) { return ; + } else if (isChecking) { + return ; } - return indexes != null ? ( + return isCheckComplete ? (
- {renderTabs()} - {selectedTabContent} + {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/data_quality_panel/index_properties/index_check_fields/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_check_fields/index.test.tsx new file mode 100644 index 0000000000000..e5ab5247ac41e --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_check_fields/index.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { 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'; + +describe('IndexCheckFields', () => { + beforeEach(() => { + render( + + + + + + ); + }); + it('should render the index check fields', () => { + expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument(); + }); + + it('should render incompatible tab content by default', () => { + expect(screen.getByTestId('incompatibleTab')).toBeInTheDocument(); + expect(screen.getByTestId('incompatibleTabContent')).toBeInTheDocument(); + }); + + describe.each([ + ['sameFamilyTab', 'sameFamilyTabContent'], + ['customTab', 'customTabContent'], + ['ecsCompliantTab', 'ecsCompliantTabContent'], + ['allTab', 'allTabContent'], + ])('when clicking on %s tab', (tab, tabContent) => { + it(`should render ${tabContent} content`, () => { + userEvent.click(screen.getByTestId(tab)); + + expect(screen.getByTestId(tabContent)).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_check_fields/index.tsx new file mode 100644 index 0000000000000..8fa4a37ea3ead --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_check_fields/index.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState } from 'react'; +import { EuiButtonGroup, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; + +import { useDataQualityContext } from '../../data_quality_context'; +import { useIndicesCheckContext } from '../../../contexts/indices_check_context'; +import { EMPTY_METADATA, INCOMPATIBLE_TAB_ID } from '../helpers'; +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/data_quality_panel/index_properties/index_stats_panel/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_stats_panel/index.test.tsx new file mode 100644 index 0000000000000..f878cd9de4f13 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_stats_panel/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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/data_quality_panel/index_properties/index_stats_panel/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_stats_panel/index.tsx new file mode 100644 index 0000000000000..03b20e8a4ce45 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index_stats_panel/index.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 } from '../translations'; +import { ILM_PHASE } from '../../../translations'; +import { SIZE } from '../../summary_table/translations'; +import { Stat } from '../../pattern/pattern_summary/stats_rollup/stat'; +import { getIlmPhaseDescription } from '../../../helpers'; + +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/data_quality_panel/index_properties/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/translations.ts index e519cb234c62e..7e18a0941e004 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/translations.ts @@ -217,6 +217,13 @@ export const ERROR_LOADING_UNALLOWED_VALUES_TITLE = i18n.translate( } ); +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', @@ -240,10 +247,10 @@ export const LOADING_UNALLOWED_VALUES = i18n.translate( } ); -export const SUMMARY = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryTab', +export const CHECKING_INDEX = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.checkingIndexPrompt', { - defaultMessage: 'Summary', + defaultMessage: 'Checking index', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.test.ts new file mode 100644 index 0000000000000..2f85a6e0ce692 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.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 { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../summary_table/translations'; +import { getIndexResultBadgeColor, getIndexResultToolTip } from './helpers'; + +describe('getIndexResultBadgeColor', () => { + test('it returns `ghost` when `incompatible` is undefined', () => { + expect(getIndexResultBadgeColor(undefined)).toEqual('ghost'); + }); + + test('it returns `success` when `incompatible` is zero', () => { + expect(getIndexResultBadgeColor(0)).toEqual('#6dcbb1'); + }); + + test('it returns `danger` when `incompatible` is NOT zero', () => { + expect(getIndexResultBadgeColor(1)).toEqual('danger'); + }); +}); + +describe('getIndexResultToolTip', () => { + test('it returns "this index has not been checked" when `incompatible` is undefined', () => { + expect(getIndexResultToolTip(undefined)).toEqual(THIS_INDEX_HAS_NOT_BEEN_CHECKED); + }); + + test('it returns Passed when `incompatible` is zero', () => { + expect(getIndexResultToolTip(0)).toEqual(PASSED); + }); + + test('it returns Failed when `incompatible` is NOT zero', () => { + expect(getIndexResultToolTip(1)).toEqual(FAILED); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.ts new file mode 100644 index 0000000000000..f9f1c69733059 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/helpers.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../summary_table/translations'; + +export const getIndexResultBadgeColor = (incompatible: number | undefined): string => { + if (incompatible == null) { + return 'ghost'; + } else if (incompatible === 0) { + return '#6dcbb1'; + } else { + return 'danger'; + } +}; + +export const getIndexResultToolTip = (incompatible: number | undefined): string => { + if (incompatible == null) { + return THIS_INDEX_HAS_NOT_BEEN_CHECKED; + } else if (incompatible === 0) { + return PASSED; + } else { + return FAILED; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.test.tsx new file mode 100644 index 0000000000000..797b54bc26b91 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { IndexResultBadge } from '.'; + +describe('IndexResultBadge', () => { + it('should render the index result badge', () => { + render(); + + expect(screen.getByTestId('indexResultBadge')).toHaveTextContent('Pass'); + }); + + describe('when incompatible is > 0', () => { + it('should render the index result badge with `Fail` content', () => { + render(); + + expect(screen.getByTestId('indexResultBadge')).toHaveTextContent('Fail'); + }); + }); + + describe('when tooltipText is given', () => { + it('should render the index result badge with the given tooltip text', async () => { + render(); + + userEvent.hover(screen.getByTestId('indexResultBadge')); + + await waitFor(() => expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip text')); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.tsx new file mode 100644 index 0000000000000..9dcd3ca18e729 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/index.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { getIndexResultBadgeColor, getIndexResultToolTip } from './helpers'; +import * as i18n from './translations'; + +const StyledBadge = styled(EuiBadge)` + width: 44px; + text-align: center; + padding-inline: 0; + + .euiBadge__content { + justify-content: center; + } +`; + +export type Props = React.ComponentProps & { + incompatible: number; + tooltipText?: string; +}; + +export const IndexResultBadgeComponent: React.FC = ({ + incompatible, + tooltipText, + ...props +}) => { + return ( + + + {incompatible > 0 ? i18n.FAIL : i18n.PASS} + + + ); +}; + +IndexResultBadgeComponent.displayName = 'IndexCheckResultBadgeComponent'; + +export const IndexResultBadge = React.memo(IndexResultBadgeComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/translations.ts new file mode 100644 index 0000000000000..79dfb1bcaac22 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_result_badge/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FAIL = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.fail', + { + defaultMessage: 'Fail', + } +); + +export const PASS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.pass', + { + defaultMessage: 'Pass', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts index efc4f3a5a5092..f6a01533bcfbf 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts @@ -28,9 +28,9 @@ import { mockIlmExplain } from '../../mock/ilm_explain/mock_ilm_explain'; import { mockDataQualityCheckResult } from '../../mock/data_quality_check_result/mock_index'; import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { mockStats } from '../../mock/stats/mock_stats'; -import { IndexSummaryTableItem } from '../summary_table/helpers'; import { DataQualityCheckResult } from '../../types'; import { getIndexNames, getTotalDocsCount } from '../../helpers'; +import { IndexSummaryTableItem } from './types'; const hot: IlmExplainLifecycleLifecycleExplainManaged = { index: '.ds-packetbeat-8.6.1-2023.02.04-000001', diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts index 1c64e1eb00958..ab3e917246f1d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts @@ -8,7 +8,6 @@ import type { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; import { isEqual, orderBy } from 'lodash/fp'; -import type { IndexSummaryTableItem } from '../summary_table/helpers'; import type { IlmPhase, IlmExplainPhaseCounts, @@ -18,6 +17,7 @@ import type { MeteringStatsIndex, } from '../../types'; import { getDocsCount, getSizeInBytes } from '../../helpers'; +import { IndexSummaryTableItem } from './types'; export const isManaged = ( ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx index e927c969ae976..9fdd6f3acc606 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx @@ -5,15 +5,19 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; import numeral from '@elastic/numeral'; import { render, screen } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { EMPTY_STAT } from '../../helpers'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import { Pattern } from '.'; +import { getCheckState } from '../../stub/get_check_state'; +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; @@ -38,23 +42,14 @@ jest.mock('../../use_ilm_explain', () => ({ })), })); +const ilmPhases = ['hot', 'warm', 'unmanaged']; + const defaultProps: ComponentProps = { pattern: '', - addSuccessToast: jest.fn(), - canUserCreateAndReadCases: jest.fn(), - formatBytes, - formatNumber, - getGroupByFieldsOnClick: jest.fn(), - ilmPhases: ['hot', 'warm', 'unmanaged'], - indexNames: undefined, - isAssistantEnabled: true, - openCreateCaseFlyout: jest.fn(), patternRollup: undefined, - selectedIndex: null, - setSelectedIndex: jest.fn(), - baseTheme: DARK_THEME, - updatePatternIndexNames: jest.fn(), - updatePatternRollup: jest.fn(), + chartSelectedIndex: null, + setChartSelectedIndex: jest.fn(), + indexNames: undefined, }; describe('pattern', () => { @@ -66,9 +61,20 @@ describe('pattern', () => { const pattern = 'remote:*'; // <-- a colon in the pattern indicates the use of cross cluster search render( - - - + + + + + ); expect(screen.getByTestId('remoteClustersCallout')).toBeInTheDocument(); @@ -78,9 +84,20 @@ describe('pattern', () => { const pattern = 'auditbeat-*'; // <-- no colon in the pattern render( - - - + + + + + ); expect(screen.queryByTestId('remoteClustersCallout')).not.toBeInTheDocument(); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx index d54e2bf12fae4..106d9bbe2e433 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx @@ -5,26 +5,13 @@ * 2.0. */ -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiSpacer, useGeneratedHtmlId } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import styled from 'styled-components'; import { ErrorEmptyPrompt } from '../error_empty_prompt'; import { defaultSort, getIlmExplainPhaseCounts, - getIlmPhase, getPageIndex, getSummaryTableItems, MIN_PAGE_SIZE, @@ -32,15 +19,12 @@ import { shouldCreatePatternRollup, } from './helpers'; import { - getDocsCount, - getIndexId, getIndexNames, getTotalDocsCount, getTotalPatternIncompatible, getTotalPatternIndicesChecked, getTotalSizeInBytes, } from '../../helpers'; -import { IndexProperties } from '../index_properties'; import { LoadingEmptyPrompt } from '../loading_empty_prompt'; import { PatternSummary } from './pattern_summary'; import { RemoteClustersCallout } from '../remote_clusters_callout'; @@ -51,86 +35,41 @@ import type { PatternRollup, SelectedIndex, SortConfig } from '../../types'; import { useIlmExplain } from '../../use_ilm_explain'; import { useStats } from '../../use_stats'; import { useDataQualityContext } from '../data_quality_context'; - -const IndexPropertiesContainer = styled.div` - margin-bottom: ${euiThemeVars.euiSizeS}; - width: 100%; -`; +import { PatternAccordion, PatternAccordionChildren } from './styles'; +import { IndexCheckFlyout } from './index_check_flyout'; +import { useResultsRollupContext } from '../../contexts/results_rollup_context'; +import { useIndicesCheckContext } from '../../contexts/indices_check_context'; const EMPTY_INDEX_NAMES: string[] = []; interface Props { - addSuccessToast: (toast: { title: string }) => void; - baseTheme: Theme; - canUserCreateAndReadCases: () => boolean; - endDate?: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - ilmPhases: string[]; indexNames: string[] | undefined; - isAssistantEnabled: boolean; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; pattern: string; patternRollup: PatternRollup | undefined; - selectedIndex: SelectedIndex | null; - setSelectedIndex: (selectedIndex: SelectedIndex | null) => void; - startDate?: string | null; - theme?: PartialTheme; - updatePatternIndexNames: ({ - indexNames, - pattern, - }: { - indexNames: string[]; - pattern: string; - }) => void; - updatePatternRollup: (patternRollup: PatternRollup, requestTime?: number) => void; + chartSelectedIndex: SelectedIndex | null; + setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void; } const PatternComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - endDate, - formatBytes, - formatNumber, - getGroupByFieldsOnClick, indexNames, - ilmPhases, - isAssistantEnabled, - openCreateCaseFlyout, pattern, patternRollup, - selectedIndex, - setSelectedIndex, - startDate, - theme, - baseTheme, - updatePatternIndexNames, - updatePatternRollup, + chartSelectedIndex, + setChartSelectedIndex, }) => { + const { httpFetch, isILMAvailable, ilmPhases, startDate, endDate, formatBytes, formatNumber } = + useDataQualityContext(); + const { checkIndex, checkState } = useIndicesCheckContext(); + const { updatePatternIndexNames, updatePatternRollup } = useResultsRollupContext(); const containerRef = useRef(null); - const { isILMAvailable } = useDataQualityContext(); const [sorting, setSorting] = useState(defaultSort); const [pageIndex, setPageIndex] = useState(0); 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 { error: statsError, @@ -145,70 +84,13 @@ const PatternComponent: React.FC = ({ ); const error = useMemo(() => statsError ?? ilmExplainError, [ilmExplainError, statsError]); - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< - Record - >({}); - - const toggleExpanded = useCallback( - (indexName: string) => { - if (itemIdToExpandedRowMap[indexName]) { - setItemIdToExpandedRowMap({}); - } else { - setItemIdToExpandedRowMap({ - [indexName]: ( - - - - ), - }); - } - }, - [ - itemIdToExpandedRowMap, - addSuccessToast, - canUserCreateAndReadCases, - formatBytes, - formatNumber, - stats, - getGroupByFieldsOnClick, - ilmExplain, - isILMAvailable, - isAssistantEnabled, - openCreateCaseFlyout, - pattern, - patternRollup, - theme, - baseTheme, - updatePatternRollup, - ] - ); - const ilmExplainPhaseCounts = useMemo( () => (isILMAvailable ? getIlmExplainPhaseCounts(ilmExplain) : undefined), [ilmExplain, isILMAvailable] ); + const isFlyoutVisible = expandedIndexName !== null; + const items = useMemo( () => getSummaryTableItems({ @@ -235,6 +117,39 @@ const PatternComponent: React.FC = ({ ] ); + const handleFlyoutClose = useCallback(() => { + setExpandedIndexName(null); + }, []); + + const handleFlyoutIndexExpandAction = useCallback( + (indexName) => { + checkIndex({ + abortController: flyoutIndexExpandActionAbortControllerRef.current, + indexName, + pattern, + httpFetch, + formatBytes, + formatNumber, + }); + setExpandedIndexName(indexName); + }, + [checkIndex, formatBytes, formatNumber, httpFetch, pattern] + ); + + const handleTableRowIndexCheckNowAction = useCallback( + (indexName) => { + checkIndex({ + abortController: tableRowIndexCheckNowActionAbortControllerRef.current, + indexName, + pattern, + httpFetch, + formatBytes, + formatNumber, + }); + }, + [checkIndex, formatBytes, formatNumber, httpFetch, pattern] + ); + useEffect(() => { const newIndexNames = getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable }); const newDocsCount = getTotalDocsCount({ indexNames: newIndexNames, stats }); @@ -296,9 +211,9 @@ const PatternComponent: React.FC = ({ ]); useEffect(() => { - if (selectedIndex?.pattern === pattern) { + if (chartSelectedIndex?.pattern === pattern) { const selectedPageIndex = getPageIndex({ - indexName: selectedIndex.indexName, + indexName: chartSelectedIndex.indexName, items, pageSize, }); @@ -307,30 +222,57 @@ const PatternComponent: React.FC = ({ setPageIndex(selectedPageIndex); } - if (itemIdToExpandedRowMap[selectedIndex.indexName] == null) { - toggleExpanded(selectedIndex.indexName); // expand the selected index + if (chartSelectedIndex.indexName !== expandedIndexName && !isFlyoutVisible) { + checkIndex({ + abortController: flyoutIndexChartSelectedActionAbortControllerRef.current, + indexName: chartSelectedIndex.indexName, + pattern: chartSelectedIndex.pattern, + httpFetch, + formatBytes, + formatNumber, + }); + setExpandedIndexName(chartSelectedIndex.indexName); } containerRef.current?.scrollIntoView(); - setSelectedIndex(null); + setChartSelectedIndex(null); } }, [ - itemIdToExpandedRowMap, items, pageSize, pattern, - selectedIndex, - setSelectedIndex, - toggleExpanded, + chartSelectedIndex, + setChartSelectedIndex, + expandedIndexName, + isFlyoutVisible, + checkIndex, + httpFetch, + formatBytes, + formatNumber, ]); + useEffect(() => { + const flyoutIndexExpandActionAbortController = + flyoutIndexExpandActionAbortControllerRef.current; + const tableRowIndexCheckNowActionAbortController = + tableRowIndexCheckNowActionAbortControllerRef.current; + const flyoutIndexChartSelectedActionAbortController = + flyoutIndexChartSelectedActionAbortControllerRef.current; + return () => { + flyoutIndexExpandActionAbortController.abort(); + tableRowIndexCheckNowActionAbortController.abort(); + flyoutIndexChartSelectedActionAbortController.abort(); + }; + }, []); + return ( - - - +
+ = ({ patternDocsCount={patternRollup?.docsCount ?? 0} patternSizeInBytes={patternRollup?.sizeInBytes} /> - - + } + > + + {!loading && pattern.includes(':') && ( + <> + + + + )} - {!loading && pattern.includes(':') && ( - <> - - - - )} + {!loading && error != null && ( + <> + + + + )} - {!loading && error != null && ( - - )} + {loading && ( + <> + + + + )} - {loading && } - - {!loading && error == null && ( -
- -
- )} - - + {!loading && error == null && ( +
+ +
+ )} +
+
+ {isFlyoutVisible ? ( + + ) : null} +
); }; +PatternComponent.displayName = 'PatternComponent'; + export const Pattern = React.memo(PatternComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/index.test.tsx new file mode 100644 index 0000000000000..b73da31bbcf8c --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/index.test.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 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, +} 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'; + +describe('IndexCheckFlyout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + beforeEach(() => { + render( + + + + + + ); + }); + + it('should render without crashing', () => { + expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument(); + }); + + it('should render heading section correctly with formatted latest check time', () => { + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( + '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' + ) + ); + }); + + it('should render the correct index properties panel', () => { + expect(screen.getByTestId('indexStatsPanel')).toBeInTheDocument(); + expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument(); + }); + + it('should render footer with check now button', () => { + expect(screen.getByRole('button', { name: 'Check now' })).toBeInTheDocument(); + }); + }); + + describe('when flyout close is clicked', () => { + it('should call onClose', () => { + const onClose = jest.fn(); + render( + + + + + + ); + + const closeButton = screen.getByRole('button', { name: 'Close this dialog' }); + userEvent.click(closeButton); + + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe('when check now button is clicked', () => { + it('should call checkIndex', () => { + const checkIndex = jest.fn(); + render( + + + + + + ); + + const checkNowButton = screen.getByRole('button', { name: 'Check now' }); + userEvent.click(checkNowButton); + + expect(checkIndex).toHaveBeenCalledWith({ + abortController: expect.any(AbortController), + formatBytes: expect.any(Function), + formatNumber: expect.any(Function), + httpFetch: expect.any(Function), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/index.tsx new file mode 100644 index 0000000000000..b9d33c51cc495 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/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 { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import moment from 'moment'; +import { useIndicesCheckContext } from '../../../contexts/indices_check_context'; + +import { EMPTY_STAT, getDocsCount, getSizeInBytes } from '../../../helpers'; +import { MeteringStatsIndex, PatternRollup } from '../../../types'; +import { useDataQualityContext } from '../../data_quality_context'; +import { IndexProperties } from '../../index_properties'; +import { getIlmPhase } from '../helpers'; +import { IndexResultBadge } from '../../index_result_badge'; +import { useCurrentWindowWidth } from '../../../use_current_window_width'; +import { CHECK_NOW } from './translations'; + +export interface Props { + ilmExplain: Record | null; + indexName: string; + pattern: string; + patternRollup: PatternRollup | undefined; + stats: Record | null; + onClose: () => void; +} + +export const IndexCheckFlyoutComponent: React.FC = ({ + ilmExplain, + indexName, + pattern, + patternRollup, + stats, + onClose, +}) => { + const currentWindowWidth = useCurrentWindowWidth(); + const isLargeScreen = currentWindowWidth > 1720; + const isMediumScreen = currentWindowWidth > 1200; + const { httpFetch, formatBytes, formatNumber, isILMAvailable } = useDataQualityContext(); + const { checkState, checkIndex } = useIndicesCheckContext(); + const indexCheckState = checkState[indexName]; + const isChecking = indexCheckState?.isChecking ?? false; + const partitionedFieldMetadata = indexCheckState?.partitionedFieldMetadata ?? null; + const indexResult = patternRollup?.results?.[indexName]; + const indexCheckFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'indexCheckFlyoutTitle', + }); + const abortControllerRef = React.useRef(new AbortController()); + + const handleCheckNow = useCallback(() => { + checkIndex({ + abortController: abortControllerRef.current, + indexName, + pattern, + httpFetch, + formatBytes, + formatNumber, + }); + }, [checkIndex, formatBytes, formatNumber, httpFetch, indexName, pattern]); + + useEffect(() => { + const abortController = abortControllerRef.current; + return () => { + abortController.abort(); + }; + }, []); + + return ( +
+ + + + + {partitionedFieldMetadata?.incompatible != null && ( + + )} +

{indexName}

+
+
+ {indexResult != null && indexResult.checkedAt != null && ( + <> + + + {moment(indexResult.checkedAt).isValid() + ? moment(indexResult.checkedAt).format('MMM DD, YYYY @ HH:mm:ss.SSS') + : EMPTY_STAT} + + + )} +
+ + + + + + + + {CHECK_NOW} + + + + +
+
+ ); +}; + +IndexCheckFlyoutComponent.displayName = 'IndexCheckFlyoutComponent'; + +export const IndexCheckFlyout = React.memo(IndexCheckFlyoutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/translations.ts new file mode 100644 index 0000000000000..e5915404cba02 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index_check_flyout/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const CHECK_NOW: string = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.checkNowButton', + { + defaultMessage: 'Check now', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx index 722f56ca721d8..c30fd3e7dc4ce 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx @@ -13,8 +13,6 @@ import { PatternLabel } from './pattern_label'; import { StatsRollup } from './stats_rollup'; interface Props { - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; ilmExplainPhaseCounts: IlmExplainPhaseCounts | undefined; incompatible: number | undefined; indices: number | undefined; @@ -25,8 +23,6 @@ interface Props { } const PatternSummaryComponent: React.FC = ({ - formatBytes, - formatNumber, ilmExplainPhaseCounts, incompatible, indices, @@ -35,7 +31,7 @@ const PatternSummaryComponent: React.FC = ({ patternDocsCount, patternSizeInBytes, }) => ( - + = ({ { - describe('getResultToolTip', () => { + describe('getPatternResultTooltip', () => { test('it returns the expected tool tip when `incompatible` is undefined', () => { - expect(getResultToolTip(undefined)).toEqual(SOME_UNCHECKED); + expect(getPatternResultTooltip(undefined)).toEqual(SOME_UNCHECKED); }); test('it returns the expected tool tip when `incompatible` is zero', () => { - expect(getResultToolTip(0)).toEqual(ALL_PASSED); + expect(getPatternResultTooltip(0)).toEqual(ALL_PASSED); }); test('it returns the expected tool tip when `incompatible` is non-zero', () => { - expect(getResultToolTip(1)).toEqual(SOME_FAILED); + expect(getPatternResultTooltip(1)).toEqual(SOME_FAILED); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.ts index 8b3ad21adcbfd..bce8098e963fc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.ts @@ -7,7 +7,7 @@ import * as i18n from './translations'; -export const getResultToolTip = (incompatible: number | undefined): string => { +export const getPatternResultTooltip = (incompatible: number | undefined): string => { if (incompatible == null) { return i18n.SOME_UNCHECKED; } else if (incompatible === 0) { @@ -16,14 +16,16 @@ export const getResultToolTip = (incompatible: number | undefined): string => { return i18n.SOME_FAILED; } }; +interface ShowResultProps { + incompatible: T; + indices: T; + indicesChecked: T; +} -export const showResult = ({ - incompatible, - indices, - indicesChecked, -}: { - incompatible: number | undefined; - indices: number | undefined; - indicesChecked: number | undefined; -}): boolean => - incompatible != null && indices != null && indicesChecked != null && indices === indicesChecked; +export const showResult = ( + opts: ShowResultProps +): opts is ShowResultProps> => + opts.incompatible != null && + opts.indices != null && + opts.indicesChecked != null && + opts.indices === opts.indicesChecked; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/index.tsx index 24a23c55e83ac..62ff66a873124 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/index.tsx @@ -5,19 +5,14 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; -import { getResultToolTip, showResult } from './helpers'; +import { getPatternResultTooltip, showResult } from './helpers'; import { IlmPhaseCounts } from '../../../ilm_phase_counts'; -import { getResultIcon, getResultIconColor } from '../../../summary_table/helpers'; import * as i18n from '../translations'; import type { IlmExplainPhaseCounts } from '../../../../types'; - -const ResultContainer = styled.div` - margin-right: ${({ theme }) => theme.eui.euiSizeS}; -`; +import { IndexResultBadge } from '../../../index_result_badge'; interface Props { incompatible: number | undefined; @@ -33,41 +28,46 @@ const PatternLabelComponent: React.FC = ({ indices, indicesChecked, pattern, -}) => ( - <> - - - - {showResult({ - incompatible, - indices, - indicesChecked, - }) && ( - - - - )} - - +}) => { + // this is a workaround for type guard limitation + // TS does not type narrow value passed to the type guard function + // if that value is proxied via another key like for example {incompatible: *incompatible*} + // so we need a reference object to pass it to the type guard + // and then check the keys of that object (resultOpts) for type guarded result + // to be properly type narrowed instead + const resultOpts = { + incompatible, + indices, + indicesChecked, + }; + + return ( + + {showResult(resultOpts) && ( + + + + )} - +

{pattern}

-
- - {ilmExplainPhaseCounts && ( - - )} - -); + {ilmExplainPhaseCounts && ( + + + + )} +
+ ); +}; PatternLabelComponent.displayName = 'PatternLabelComponent'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/translations.ts index 165dc45b3f1c0..c53602120a9d8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/translations.ts @@ -17,13 +17,14 @@ export const ALL_PASSED = i18n.translate( export const SOME_FAILED = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip', { - defaultMessage: 'Some indices matching this pattern failed the data quality checks', + defaultMessage: 'At least one index matching this pattern failed a data quality check', } ); export const SOME_UNCHECKED = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip', { - defaultMessage: 'Some indices matching this pattern have not been checked for data quality', + defaultMessage: + 'At least one index matching this pattern has not been checked for data quality', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.test.tsx new file mode 100644 index 0000000000000..ba470748727bc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.test.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../../../mock/test_providers/test_providers'; +import { StatsRollup } from '.'; + +describe('StatsRollup', () => { + it('should render properly formatted stats rollup', () => { + render( + + + + + + ); + + const container = screen.getByTestId('statsRollup'); + + expect(container).toHaveTextContent('Incompatible fields2,222,222'); + expect(container).toHaveTextContent('Indices checked4,444,444'); + expect(container).toHaveTextContent('Indices3,333,333'); + expect(container).toHaveTextContent('Size5.3MB'); + expect(container).toHaveTextContent('Docs1,111,111'); + }); + + describe.each([ + ['Docs', '--'], + ['Incompatible fields', '--'], + ['Indices', '0'], + ['Indices checked', '--'], + ])('when %s count is not provided', (statLabel, emptyText) => { + it(`should render empty ${statLabel} stat`, () => { + render( + + + + + + ); + + const container = screen.getByTestId('statsRollup'); + + expect(container).toHaveTextContent(`${statLabel}${emptyText}`); + }); + }); + + describe('when size count is not provided', () => { + it('should not render size stat', () => { + render( + + + + + + ); + + const container = screen.getByTestId('statsRollup'); + + expect(container).not.toHaveTextContent('Size'); + }); + }); + + describe.each([ + [ + 'Docs', + 'Total number of docs in indices matching this index pattern', + 'Total number of docs in all indices', + ], + [ + 'Incompatible fields', + 'Total number of checked fields incompatible with ECS in indices matching this index pattern', + 'Total number of checked fields incompatible with ECS', + ], + ['Indices', 'Total number of indices matching this index pattern', 'Total number of indices'], + [ + 'Indices checked', + 'Total number of checked indices matching this index pattern', + 'Total number of checked indices', + ], + [ + 'Size', + 'Total size of indices (excluding replicas) matching this index pattern', + 'Total size of indices (excluding replicas)', + ], + ])('%s count tooltips', (statLabelText, patternTooltipText, noPatternTooltipText) => { + describe('when pattern is provided', () => { + it('should render pattern specific tooltip', async () => { + render( + + + + + + ); + + userEvent.hover(screen.getByText(statLabelText)); + + await waitFor(() => + expect(screen.getByRole('tooltip')).toHaveTextContent( + patternTooltipText.replace('{pattern}', 'my-pattern') + ) + ); + }); + }); + + describe('when pattern is not provided', () => { + it('should render default tooltip', async () => { + render( + + + + + + ); + + userEvent.hover(screen.getByText(statLabelText)); + + await waitFor(() => + expect(screen.getByRole('tooltip')).toHaveTextContent(noPatternTooltipText) + ); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx index 8925eb8118dc5..0a36dbf0e3ab3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx @@ -5,152 +5,116 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import styled from 'styled-components'; -import { EMPTY_STAT, getIncompatibleStatColor } from '../../../../helpers'; -import { StatLabel } from '../../../stat_label'; +import { EMPTY_STAT, getIncompatibleStatBadgeColor } from '../../../../helpers'; +import { useDataQualityContext } from '../../../data_quality_context'; import * as i18n from '../../../stat_label/translations'; +import { Stat } from './stat'; -const IndicesStatContainer = styled.div` - min-width: 100px; -`; +const StyledStatWrapperFlexItem = styled(EuiFlexItem)` + padding: 0 ${({ theme }) => theme.eui.euiSize}; + border-right: ${({ theme }) => theme.eui.euiBorderThin}; + border-color: ${({ theme }) => theme.eui.euiBorderColor}; -const DocsContainer = styled.div` - min-width: 155px; + &:last-child { + padding-right: 0; + border-right: none; + } + &:first-child { + padding-left: 0; + } `; -const STAT_TITLE_SIZE = 's'; - interface Props { - docsCount: number | undefined; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - incompatible: number | undefined; - indices: number | undefined; - indicesChecked: number | undefined; + docsCount?: number; + incompatible?: number; + indices?: number; + indicesChecked?: number; pattern?: string; - sizeInBytes: number | undefined; + sizeInBytes?: number; } const StatsRollupComponent: React.FC = ({ docsCount, - formatBytes, - formatNumber, incompatible, indices, indicesChecked, pattern, sizeInBytes, }) => { - const incompatibleDescription = useMemo( - () => , - [] - ); - const indicesCheckedDescription = useMemo( - () => , - [] - ); - const sizeDescription = useMemo(() => , []); - const docsDescription = useMemo(() => , []); - const indicesDescription = useMemo(() => , []); + const { formatNumber, formatBytes } = useDataQualityContext(); return ( - - + - - - + {i18n.INCOMPATIBLE_FIELDS} + + - - - - - - - + + + {i18n.INDICES_CHECKED} + + - - - - - - - + + + {i18n.INDICES} + + {sizeInBytes != null && ( - - - - - - - - )} - - - - + - - - - + {i18n.SIZE} + + + )} + + + + {i18n.DOCS} + + ); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/index.test.tsx new file mode 100644 index 0000000000000..6979be1c8af32 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/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 React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { Props, Stat, arePropsEqualOneLevelDeep } from '.'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; + +describe('Stat', () => { + it('renders stat with badge', () => { + render(); + expect(screen.getByTestId('stat')).toHaveTextContent('thebadge'); + }); + + it('renders stat with tooltip', async () => { + render(); + userEvent.hover(screen.getByText('thebadge')); + expect(screen.getByText('thebadge')).toBeInTheDocument(); + await waitFor(() => expect(screen.getByText('thetooltip')).toBeInTheDocument()); + }); + + it('renders stat with children', () => { + render( + + + {'thechildren'} + + + ); + expect(screen.getByText('thechildren')).toBeInTheDocument(); + expect(screen.getByText('thebadge')).toBeInTheDocument(); + }); +}); + +describe('arePropsEqualOneLevelDeep', () => { + describe('when badgeProps are equal', () => { + it('returns true', () => { + const prevProps = { badgeProps: { color: 'hollow' } } as Props; + const nextProps = { badgeProps: { color: 'hollow' } } as Props; + + expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(true); + }); + }); + + describe('when badgeProps are not equal', () => { + it('returns false', () => { + const prevProps = { badgeProps: { color: 'hollow' } } as Props; + const nextProps = { badgeProps: { color: 'primary' } } as Props; + + expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(false); + }); + }); + + describe('when other props are passed', () => { + describe('when props are equal', () => { + it('returns true', () => { + const prevProps = { badgeText: '1' } as Props; + const nextProps = { badgeText: '1' } as Props; + + expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(true); + }); + }); + + describe('when props are not equal', () => { + it('returns false', () => { + const prevProps = { badgeText: '1' } as Props; + const nextProps = { badgeText: '2' } as Props; + + expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/index.tsx new file mode 100644 index 0000000000000..a7d9286561a5b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/stat/index.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge, EuiText, EuiToolTip } from '@elastic/eui'; +import styled from 'styled-components'; + +const StyledText = styled(EuiText)` + white-space: nowrap; +`; + +const StyledDescription = styled.span` + margin-right: ${({ theme }) => theme.eui.euiSizeS}; + vertical-align: baseline; +`; + +export interface Props { + badgeText: string; + badgeColor?: string; + tooltipText?: string; + children?: React.ReactNode; + badgeProps?: React.ComponentProps; +} + +const StatComponent: React.FC = ({ + badgeColor = 'hollow', + badgeText, + tooltipText, + children, + badgeProps, +}) => { + return ( + + + {children && {children}} + + {badgeText} + + + + ); +}; + +StatComponent.displayName = 'StatComponent'; + +// The badgeProps object requires a deeper level of comparison than the default shallow comparison. +// However, using _.isEqual for this purpose would be excessive. +// The other properties should continue to be checked shallowly. +// In essence, only badgeProps needs a deeper comparison, +// while the remaining properties can be compared using React's internal Object.is comparison. +export const arePropsEqualOneLevelDeep = (prevProps: T, nextProps: T): boolean => { + for (const key of Object.keys(prevProps) as Array) { + if (key === 'badgeProps') { + const prevValue = prevProps[key]; + const nextValue = nextProps[key]; + if (prevValue && nextValue) { + return arePropsEqualOneLevelDeep( + prevValue as unknown as Props, + nextValue as unknown as Props + ); + } + } + + if (!Object.is(prevProps[key], nextProps[key])) { + return false; + } + } + + return true; +}; + +export const Stat = React.memo(StatComponent, arePropsEqualOneLevelDeep); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/translations.ts index 244fc257d2797..2fdc82300d00d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/translations.ts @@ -24,6 +24,6 @@ export const INDICES = i18n.translate( export const PATTERN_OR_INDEX_TOOLTIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip', { - defaultMessage: 'A pattern or specific index', + defaultMessage: 'Index name or pattern', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/styles.tsx new file mode 100644 index 0000000000000..d2b2e7d1b4934 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/styles.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 { EuiAccordion } from '@elastic/eui'; +import styled from 'styled-components'; + +export const PatternAccordion = styled(EuiAccordion)` + .euiAccordion__triggerWrapper { + 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; + } +`; + +export const PatternAccordionChildren = styled.div` + padding: ${({ theme }) => theme.eui.euiSize}; + padding-bottom: 0; + border: 1px solid ${({ theme }) => theme.eui.euiBorderColor}; + border-radius: 0 0 ${({ theme }) => `${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius}`}; + border-top: none; + width: calc(100% - ${({ theme }) => theme.eui.euiSizeS} * 2); + margin: auto; +`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/types.ts new file mode 100644 index 0000000000000..b079976950f1b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/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 { IlmPhase } from '../../types'; + +export interface IndexSummaryTableItem { + docsCount: number; + incompatible: number | undefined; + indexName: string; + ilmPhase: IlmPhase | undefined; + pattern: string; + patternDocsCount: number; + sizeInBytes: number | undefined; + checkedAt: number | undefined; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx index 0c4a439f999bc..d1bea1a3312b3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx @@ -9,15 +9,15 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { SAME_FAMILY } from './translations'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { SameFamily } from '.'; describe('SameFamily', () => { test('it renders a badge with the expected content', () => { render( - + - + ); expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/index.tsx deleted file mode 100644 index 32402b49b570c..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/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 React from 'react'; -import styled from 'styled-components'; - -const Line1 = styled.span` - display: block; -`; - -const Line2 = styled.span` - display: inline-block; -`; - -const EMPTY = ' '; - -interface Props { - line1?: string; - line2?: string; -} - -export const StatLabel: React.FC = ({ line1 = EMPTY, line2 = EMPTY }) => ( - <> - {line1} - {line2} - -); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts index 99701d32f722a..8f654de4461e6 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts @@ -14,29 +14,6 @@ export const CHECKED = i18n.translate( } ); -export const CUSTOM = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.customLabel', - { - defaultMessage: 'Custom', - } -); - -export const CUSTOM_INDEX_TOOL_TIP = (indexName: string) => - i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.statLabels.customIndexToolTip', { - values: { indexName }, - defaultMessage: 'A count of the custom field mappings in the {indexName} index', - }); - -export const CUSTOM_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.customPatternToolTip', - { - values: { pattern }, - defaultMessage: - 'The total count of custom field mappings, in indices matching the {pattern} pattern', - } - ); - export const DOCS = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel', { @@ -44,57 +21,20 @@ export const DOCS = i18n.translate( } ); -export const FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.fieldsLabel', +export const SIZE = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel', { - defaultMessage: 'fields', + defaultMessage: 'Size', } ); -export const INCOMPATIBLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleLabel', +export const INCOMPATIBLE_FIELDS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleFieldsLabel', { - defaultMessage: 'Incompatible', + defaultMessage: 'Incompatible fields', } ); -export const INCOMPATIBLE_INDEX_TOOL_TIP = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip', - { - values: { indexName }, - defaultMessage: 'Mappings and values incompatible with ECS, in the {indexName} index', - } - ); - -export const INCOMPATIBLE_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatiblePatternToolTip', - { - values: { pattern }, - defaultMessage: - 'The total count of fields incompatible with ECS, in indices matching the {pattern} pattern', - } - ); - -export const INDEX_DOCS_COUNT_TOOL_TIP = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsCountToolTip', - { - values: { indexName }, - defaultMessage: 'A count of the docs in the {indexName} index', - } - ); - -export const INDEX_DOCS_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsPatternToolTip', - { - values: { pattern }, - defaultMessage: 'The total count of docs, in indices matching the {pattern} pattern', - } - ); - export const INDICES = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel', { @@ -102,6 +42,13 @@ export const INDICES = i18n.translate( } ); +export const INDICES_CHECKED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesCheckedLabel', + { + defaultMessage: 'Indices checked', + } +); + export const SAME_FAMILY = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel', { @@ -109,91 +56,80 @@ export const SAME_FAMILY = i18n.translate( } ); -export const SAME_FAMILY_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyPatternToolTip', - { - values: { pattern }, - defaultMessage: - 'The total count of fields in the same family as the type specified by ECS, in indices matching the {pattern} pattern', - } - ); +export const INCOMPATIBLE_INDEX_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip', + { + defaultMessage: 'Mappings and values incompatible with ECS', + } +); -export const SIZE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel', +export const TOTAL_INCOMPATIBLE_PATTERN_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatiblePatternToolTip', { - defaultMessage: 'Size', + defaultMessage: + 'Total number of checked fields incompatible with ECS in indices matching this index pattern', + } +); + +export const TOTAL_DOCS_PATTERN_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsPatternToolTip', + { + defaultMessage: 'Total number of docs in indices matching this index pattern', + } +); + +export const TOTAL_SIZE_PATTERN_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizePatternToolTip', + { + defaultMessage: 'Total size of indices (excluding replicas) matching this index pattern', + } +); + +export const TOTAL_CHECKED_INDICES_PATTERN_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesPatternToolTip', + { + defaultMessage: 'Total number of checked indices matching this index pattern', } ); -export const INDICES_SIZE_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip', - { - values: { pattern }, - defaultMessage: - 'The total size of the primary indices matching the {pattern} pattern (does not include replicas)', - } - ); - -export const TOTAL_COUNT_OF_INDICES_CHECKED_MATCHING_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip', - { - values: { pattern }, - defaultMessage: 'The total count of indices checked that match the {pattern} pattern', - } - ); - -export const TOTAL_COUNT_OF_INDICES_MATCHING_PATTERN_TOOL_TIP = (pattern: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesMatchingPatternToolTip', - { - values: { pattern }, - defaultMessage: 'The total count of indices matching the {pattern} pattern', - } - ); +export const TOTAL_INDICES_PATTERN_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesPatternToolTip', + { + defaultMessage: 'Total number of indices matching this index pattern', + } +); export const TOTAL_DOCS_TOOL_TIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip', { - defaultMessage: 'The total count of docs, in all indices', + defaultMessage: 'Total number of docs in all indices', } ); export const TOTAL_INCOMPATIBLE_TOOL_TIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip', { - defaultMessage: - 'The total count of fields incompatible with ECS, in all indices that were checked', + defaultMessage: 'Total number of checked fields incompatible with ECS', } ); -export const TOTAL_INDICES_CHECKED_TOOL_TIP = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesCheckedToolTip', +export const TOTAL_CHECKED_INDICES_TOOL_TIP = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesToolTip', { - defaultMessage: 'The total count of all indices checked', + defaultMessage: 'Total number of checked indices', } ); export const TOTAL_INDICES_TOOL_TIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip', { - defaultMessage: 'The total count of all indices', - } -); - -export const TOTAL_SAME_FAMILY_TOOL_TIP = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSameFamilyToolTip', - { - defaultMessage: - 'The total count of fields in the same family as the ECS type, in all indices that were checked', + defaultMessage: 'Total number of indices', } ); export const TOTAL_SIZE_TOOL_TIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip', { - defaultMessage: 'The total size of all primary indices (does not include replicas)', + defaultMessage: 'Total size of indices (excluding replicas)', } ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/chart_legend_item.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/chart_legend_item/index.tsx similarity index 81% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/chart_legend_item.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/chart_legend_item/index.tsx index f561ef2cb51d7..3264696e59ddd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/chart_legend_item.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/chart_legend_item/index.tsx @@ -5,14 +5,22 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; - -import { ChartLegendLink } from '../../data_quality_panel/tabs/styles'; -import { FixedWidthLegendText } from '../../styles'; +import styled from 'styled-components'; const DEFAULT_DATA_TEST_SUBJ = 'chartLegendItem'; +export const ChartLegendLink = styled(EuiLink)` + width: 100%; +`; +export const FixedWidthLegendText = styled(EuiText)<{ + $width: number | undefined; +}>` + text-align: left; + ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} +`; + interface Props { color: string | null; count: number | string; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx index c01e21e5d3dd4..5e22bc185b1c1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx @@ -20,7 +20,10 @@ import { EMPTY_STAT } from '../../helpers'; import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import type { Props } from '.'; import { StorageTreemap } from '.'; import { DEFAULT_MAX_CHART_HEIGHT } from '../tabs/styles'; @@ -54,8 +57,6 @@ const defaultProps: Props = { maxChartHeight: DEFAULT_MAX_CHART_HEIGHT, onIndexSelected, patternRollups, - patterns, - baseTheme: DARK_THEME, valueFormatter: formatBytes, }; @@ -73,9 +74,11 @@ describe('StorageTreemap', () => { jest.clearAllMocks(); render( - - - + + + + + ); }); @@ -135,9 +138,11 @@ describe('StorageTreemap', () => { beforeEach(() => { render( - - - + + + + + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx index 2edd59be93df7..e56cbadc66009 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx @@ -14,7 +14,6 @@ import type { MetricElementEvent, PartialTheme, PartitionElementEvent, - Theme, WordCloudElementEvent, XYChartElementEvent, } from '@elastic/charts'; @@ -29,10 +28,11 @@ import { getLegendItems, getPathToFlattenedBucketMap, } from '../body/data_quality_details/storage_details/helpers'; -import { ChartLegendItem } from '../../ecs_summary_donut_chart/chart_legend/chart_legend_item'; +import { ChartLegendItem } from './chart_legend_item'; import { NoData } from './no_data'; import { ChartFlexItem, LegendContainer } from '../tabs/styles'; import { PatternRollup, SelectedIndex } from '../../types'; +import { useDataQualityContext } from '../data_quality_context'; export const DEFAULT_MIN_CHART_HEIGHT = 240; // px export const LEGEND_WIDTH = 220; // px @@ -40,14 +40,11 @@ export const LEGEND_TEXT_WITH = 120; // px export interface Props { accessor: 'sizeInBytes' | 'docsCount'; - baseTheme: Theme; flattenedBuckets: FlattenedBucket[]; maxChartHeight?: number; minChartHeight?: number; onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; patternRollups: Record; - patterns: string[]; - theme?: PartialTheme; valueFormatter: (value: number) => string; } @@ -86,16 +83,14 @@ export const getGroupByFieldsOnClick = ( const StorageTreemapComponent: React.FC = ({ accessor, - baseTheme, flattenedBuckets, maxChartHeight, minChartHeight = DEFAULT_MIN_CHART_HEIGHT, onIndexSelected, patternRollups, - patterns, - theme = {}, valueFormatter, }: Props) => { + const { theme, baseTheme, patterns } = useDataQualityContext(); const fillColor = useMemo( () => theme?.background?.color ?? baseTheme.background.color, [theme?.background?.color, baseTheme.background.color] @@ -159,7 +154,7 @@ const StorageTreemapComponent: React.FC = ({ diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx index 0cf39beae7b2d..95503d7f156bd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx @@ -11,10 +11,15 @@ import React from 'react'; import * as i18n from '../translations'; import { NoData } from '.'; +import { TestExternalProviders } from '../../../mock/test_providers/test_providers'; describe('NoData', () => { test('renders the expected "no data" message', () => { - render(); + render( + + + + ); expect(screen.getByText(i18n.NO_DATA_LABEL)).toBeInTheDocument(); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx index a5edca17291d2..4e5e8a46e62fd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx @@ -19,21 +19,27 @@ interface Props { reason?: string; } +const StyledContainer = styled.div` + padding: ${({ theme }) => theme.eui.euiSizeM} 0; +`; + const NoDataComponent: React.FC = ({ reason }) => ( - - {i18n.NO_DATA_LABEL} - - - {reason != null && ( - <> - - - {reason} - - - )} + + + {i18n.NO_DATA_LABEL} + + + {reason != null && ( + <> + + + {reason} + + + )} + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx index 814f0b234b63c..a4227f0631819 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx @@ -5,28 +5,31 @@ * 2.0. */ -import { EuiScreenReaderOnly, EuiTableFieldDataColumnType } from '@elastic/eui'; +import { + CustomItemAction, + EuiTableActionsColumnType, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; import numeral from '@elastic/numeral'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { omit } from 'lodash/fp'; import React from 'react'; -import { TestProviders } from '../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../mock/test_providers/test_providers'; import { EMPTY_STAT } from '../../helpers'; import { getDocsCountPercent, - getResultIcon, - getResultIconColor, - getResultToolTip, + getIncompatibleStatColor, getShowPagination, getSummaryTableColumns, getSummaryTableILMPhaseColumn, getSummaryTableSizeInBytesColumn, getToggleButtonId, - IndexSummaryTableItem, } from './helpers'; -import { COLLAPSE, EXPAND, FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from './translations'; +import { CHECK_INDEX, VIEW_CHECK_DETAILS } from './translations'; +import { IndexSummaryTableItem } from '../pattern/types'; +import { getCheckState } from '../../stub/get_check_state'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -37,48 +40,6 @@ const formatNumber = (value: number | undefined) => value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; describe('helpers', () => { - describe('getResultToolTip', () => { - test('it shows a "this index has not been checked" tool tip when `incompatible` is undefined', () => { - expect(getResultToolTip(undefined)).toEqual(THIS_INDEX_HAS_NOT_BEEN_CHECKED); - }); - - test('it returns Passed when `incompatible` is zero', () => { - expect(getResultToolTip(0)).toEqual(PASSED); - }); - - test('it returns Failed when `incompatible` is NOT zero', () => { - expect(getResultToolTip(1)).toEqual(FAILED); - }); - }); - - describe('getResultIconColor', () => { - test('it returns `ghost` when `incompatible` is undefined', () => { - expect(getResultIconColor(undefined)).toEqual('ghost'); - }); - - test('it returns `success` when `incompatible` is zero', () => { - expect(getResultIconColor(0)).toEqual('success'); - }); - - test('it returns `danger` when `incompatible` is NOT zero', () => { - expect(getResultIconColor(1)).toEqual('danger'); - }); - }); - - describe('getResultIcon', () => { - test('it returns `cross` when `incompatible` is undefined', () => { - expect(getResultIcon(undefined)).toEqual('cross'); - }); - - test('it returns `check` when `incompatible` is zero', () => { - expect(getResultIcon(0)).toEqual('check'); - }); - - test('it returns `cross` when `incompatible` is NOT zero', () => { - expect(getResultIcon(1)).toEqual('cross'); - }); - }); - describe('getDocsCountPercent', () => { test('it returns an empty string when `patternDocsCount` is zero', () => { expect( @@ -156,22 +117,28 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }).map((x) => omit('render', x)); expect(columns).toEqual([ { - align: 'right', - isExpander: true, - name: ( - - {'Expand rows'} - - ), - width: '40px', + name: 'Actions', + align: 'center', + width: '65px', + actions: [ + { + name: 'View check details', + render: expect.any(Function), + }, + { + name: 'Check index', + render: expect.any(Function), + }, + ], }, { field: 'incompatible', @@ -180,94 +147,141 @@ describe('helpers', () => { truncateText: false, width: '65px', }, - { field: 'indexName', name: 'Index', sortable: true, truncateText: false, width: '300px' }, - { field: 'docsCount', name: 'Docs', sortable: true, truncateText: false }, + { field: 'indexName', name: 'Index', sortable: true, truncateText: false }, + { field: 'docsCount', name: 'Docs', sortable: true, truncateText: false, width: '150px' }, { field: 'incompatible', name: 'Incompatible fields', sortable: true, truncateText: false, + width: '140px', + }, + { + field: 'ilmPhase', + name: 'ILM Phase', + sortable: true, + truncateText: false, + width: '92px', + }, + { field: 'sizeInBytes', name: 'Size', sortable: true, truncateText: false, width: '67px' }, + { + field: 'checkedAt', + name: 'Last check', + sortable: true, + truncateText: false, + width: '120px', }, - { field: 'ilmPhase', name: 'ILM Phase', sortable: true, truncateText: false }, - { field: 'sizeInBytes', name: 'Size', sortable: true, truncateText: false }, - { field: 'checkedAt', name: 'Last check', sortable: true, truncateText: false }, ]); }); - describe('expand rows render()', () => { - test('it renders an Expand button when the row is NOT expanded', () => { + describe('action columns render()', () => { + test('it renders check index button', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); - const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) - .render; + const checkNowRender = ( + (columns[0] as EuiTableActionsColumnType) + .actions[1] as CustomItemAction + ).render; render( - - {expandRowsRender != null && - expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} - + + {checkNowRender != null && checkNowRender(indexSummaryTableItem, true)} + ); - expect(screen.getByLabelText(EXPAND)).toBeInTheDocument(); + expect(screen.getByLabelText(CHECK_INDEX)).toBeInTheDocument(); }); - test('it renders a Collapse button when the row is expanded', () => { - const itemIdToExpandedRowMap: Record = { - [indexName]: () => null, - }; + test('it invokes the `onCheckNowAction` with the index name when the check index button is clicked', () => { + const onCheckNowAction = jest.fn(); const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction, + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); - const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) - .render; + const checkNowRender = ( + (columns[0] as EuiTableActionsColumnType) + .actions[1] as CustomItemAction + ).render; + + render( + + {checkNowRender != null && checkNowRender(indexSummaryTableItem, true)} + + ); + + const button = screen.getByLabelText(CHECK_INDEX); + 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( - - {expandRowsRender != null && - expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} - + + {checkNowRender != null && checkNowRender(indexSummaryTableItem, true)} + ); - expect(screen.getByLabelText(COLLAPSE)).toBeInTheDocument(); + expect(screen.getByLabelText(CHECK_INDEX)).toBeDisabled(); + expect(screen.getByLabelText('Loading')).toBeInTheDocument(); }); - test('it invokes the `toggleExpanded` with the index name when the button is clicked', () => { - const toggleExpanded = jest.fn(); + test('it invokes the `onExpandAction` with the index name when the view check details button is clicked', () => { + const onExpandAction = jest.fn(); const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded, + onCheckNowAction: jest.fn(), + onExpandAction, + checkState: getCheckState(indexName), }); - const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) - .render; + + const expandActionRender = ( + (columns[0] as EuiTableActionsColumnType) + .actions[0] as CustomItemAction + ).render; render( - - {expandRowsRender != null && - expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} - + + {expandActionRender != null && expandActionRender(indexSummaryTableItem, true)} + ); - const button = screen.getByLabelText(EXPAND); + const button = screen.getByLabelText(VIEW_CHECK_DETAILS); userEvent.click(button); - expect(toggleExpanded).toBeCalledWith(indexName); + expect(onExpandAction).toBeCalledWith(indexSummaryTableItem.indexName); }); }); @@ -281,45 +295,47 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType ).render; render( - + {incompatibleRender != null && incompatibleRender(incompatibleIsUndefined, incompatibleIsUndefined)} - + ); expect(screen.getByTestId('incompatiblePlaceholder')).toHaveTextContent(EMPTY_STAT); }); - test('it renders the expected icon when there are incompatible fields', () => { + test('it renders Fail badge when there are incompatible fields', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType ).render; render( - + {incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)} - + ); - expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'cross'); + expect(screen.getByText('Fail')).toBeInTheDocument(); }); test('it renders the expected icon when there are zero fields', () => { @@ -331,22 +347,23 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const incompatibleRender = ( columns[1] as EuiTableFieldDataColumnType ).render; render( - + {incompatibleRender != null && incompatibleRender(zeroIncompatible, zeroIncompatible)} - + ); - expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'check'); + expect(screen.getByText('Pass')).toBeInTheDocument(); }); }); @@ -355,19 +372,20 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const indexNameRender = (columns[2] as EuiTableFieldDataColumnType) .render; render( - + {indexNameRender != null && indexNameRender(indexSummaryTableItem, indexSummaryTableItem)} - + ); expect(screen.getByTestId('indexName')).toHaveTextContent(indexName); @@ -379,18 +397,19 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const docsCountRender = (columns[3] as EuiTableFieldDataColumnType) .render; render( - + {docsCountRender != null && docsCountRender(hasIncompatible, hasIncompatible)} - + ); }); @@ -414,19 +433,20 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const incompatibleRender = ( columns[4] as EuiTableFieldDataColumnType ).render; render( - + {incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)} - + ); expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('1'); @@ -436,20 +456,21 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const incompatibleRender = ( columns[4] as EuiTableFieldDataColumnType ).render; render( - + {incompatibleRender != null && incompatibleRender(indexSummaryTableItem, indexSummaryTableItem)} - + ); expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('--'); @@ -493,18 +514,19 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; render( - + {ilmPhaseRender != null && ilmPhaseRender(hasIncompatible, hasIncompatible)} - + ); expect(screen.getByTestId('ilmPhase')).toHaveTextContent('hot'); @@ -519,18 +541,19 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; render( - + {ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)} - + ); expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument(); @@ -544,18 +567,19 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable: false, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) .render; render( - + {ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)} - + ); expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument(); @@ -567,20 +591,21 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType) .render; render( - + {sizeInBytesRender != null && sizeInBytesRender(indexSummaryTableItem, indexSummaryTableItem)} - + ); expect(screen.getByTestId('sizeInBytes')).toHaveTextContent('98.6MB'); @@ -591,20 +616,21 @@ describe('helpers', () => { const columns = getSummaryTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap: {}, isILMAvailable, pattern: 'auditbeat-*', - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: getCheckState(indexName), }); const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType) .render; render( - + {sizeInBytesRender != null && sizeInBytesRender(testIndexSummaryTableItem, testIndexSummaryTableItem)} - + ); expect(screen.queryByTestId('sizeInBytes')).toBeNull(); @@ -640,4 +666,24 @@ describe('helpers', () => { ).toBe(false); }); }); + + describe('getIncompatibleStatColor', () => { + test('it returns the expected color when incompatible is greater than zero', () => { + const incompatible = 123; + + expect(getIncompatibleStatColor(incompatible)).toBe('#bd271e'); + }); + + test('it returns undefined when incompatible is zero', () => { + const incompatible = 0; + + expect(getIncompatibleStatColor(incompatible)).toBeUndefined(); + }); + + test('it returns undefined when incompatible is undefined', () => { + const incompatible = undefined; + + expect(getIncompatibleStatColor(incompatible)).toBeUndefined(); + }); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx index 07c4335c0cd17..f8e5b8d1b271e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx @@ -8,69 +8,30 @@ import { EuiBasicTableColumn, EuiText, - EuiBadge, - EuiButtonIcon, - EuiIcon, EuiProgress, - EuiScreenReaderOnly, - EuiStat, EuiToolTip, - RIGHT_ALIGNMENT, + CENTER_ALIGNMENT, + EuiButtonIcon, } from '@elastic/eui'; import React from 'react'; import moment from 'moment'; import styled from 'styled-components'; -import { EMPTY_STAT, getIlmPhaseDescription, getIncompatibleStatColor } from '../../helpers'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EMPTY_STAT, getIlmPhaseDescription } from '../../helpers'; import { INCOMPATIBLE_INDEX_TOOL_TIP } from '../stat_label/translations'; import { INDEX_SIZE_TOOLTIP } from '../../translations'; import * as i18n from './translations'; -import type { IlmPhase } from '../../types'; - -const STAT_TITLE_SIZE = 'xxs'; - -const EMPTY_DESCRIPTION = ' '; +import { IndexSummaryTableItem } from '../pattern/types'; +import { UseIndicesCheckCheckState } from '../../use_indices_check/types'; +import { IndexResultBadge } from '../index_result_badge'; +import { getIndexResultToolTip } from '../index_result_badge/helpers'; +import { Stat } from '../pattern/pattern_summary/stats_rollup/stat'; const ProgressContainer = styled.div` width: 150px; `; -export interface IndexSummaryTableItem { - docsCount: number; - incompatible: number | undefined; - indexName: string; - ilmPhase: IlmPhase | undefined; - pattern: string; - patternDocsCount: number; - sizeInBytes: number | undefined; - checkedAt: number | undefined; -} - -export const getResultToolTip = (incompatible: number | undefined): string => { - if (incompatible == null) { - return i18n.THIS_INDEX_HAS_NOT_BEEN_CHECKED; - } else if (incompatible === 0) { - return i18n.PASSED; - } else { - return i18n.FAILED; - } -}; - -export const getResultIconColor = ( - incompatible: number | undefined -): 'success' | 'danger' | 'ghost' => { - if (incompatible == null) { - return 'ghost'; - } else if (incompatible === 0) { - return 'success'; - } else { - return 'danger'; - } -}; - -export const getResultIcon = (incompatible: number | undefined): 'check' | 'cross' => - incompatible === 0 ? 'check' : 'cross'; - export const getDocsCountPercent = ({ docsCount, locales, @@ -108,14 +69,17 @@ export const getSummaryTableILMPhaseColumn = ( name: i18n.ILM_PHASE, render: (_, { ilmPhase }) => ilmPhase != null ? ( - - - {ilmPhase} - - + ) : null, sortable: true, truncateText: false, + width: '92px', }, ] : []; @@ -140,61 +104,76 @@ export const getSummaryTableSizeInBytesColumn = ({ ) : null, sortable: true, truncateText: false, + width: '67px', }, ] : []; +export const getIncompatibleStatColor = (incompatible: number | undefined): string | undefined => + incompatible != null && incompatible > 0 ? euiThemeVars.euiColorDanger : undefined; + export const getSummaryTableColumns = ({ formatBytes, formatNumber, - itemIdToExpandedRowMap, isILMAvailable, pattern, - toggleExpanded, + onExpandAction, + onCheckNowAction, + checkState, }: { formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; - itemIdToExpandedRowMap: Record; isILMAvailable: boolean; pattern: string; - toggleExpanded: (indexName: string) => void; + onExpandAction: (indexName: string) => void; + onCheckNowAction: (indexName: string) => void; + checkState: UseIndicesCheckCheckState; }): Array> => [ { - align: RIGHT_ALIGNMENT, - isExpander: true, - name: ( - - {i18n.EXPAND_ROWS} - - ), - render: ({ indexName }: IndexSummaryTableItem) => ( - toggleExpanded(indexName)} - iconType={itemIdToExpandedRowMap[indexName] ? 'arrowDown' : 'arrowRight'} - /> - ), - width: '40px', + name: i18n.ACTIONS, + align: CENTER_ALIGNMENT, + width: '65px', + actions: [ + { + name: i18n.VIEW_CHECK_DETAILS, + render: (item) => { + return ( + + onExpandAction(item.indexName)} + /> + + ); + }, + }, + { + name: i18n.CHECK_INDEX, + render: (item) => { + const isChecking = checkState[item.indexName]?.isChecking ?? false; + return ( + + onCheckNowAction(item.indexName)} + /> + + ); + }, + }, + ], }, { field: 'incompatible', name: i18n.RESULT, render: (_, { incompatible }) => incompatible != null ? ( - - - + ) : ( - + {EMPTY_STAT} ), @@ -214,7 +193,6 @@ export const getSummaryTableColumns = ({ ), sortable: true, truncateText: false, - width: '300px', }, { field: 'docsCount', @@ -233,24 +211,25 @@ export const getSummaryTableColumns = ({ ), sortable: true, truncateText: false, + width: '150px', }, { field: 'incompatible', name: i18n.INCOMPATIBLE_FIELDS, render: (_, { incompatible, indexName }) => ( - - + + color={getIncompatibleStatColor(incompatible)} + > + {incompatible ?? EMPTY_STAT} + ), sortable: true, truncateText: false, + width: '140px', }, ...getSummaryTableILMPhaseColumn(isILMAvailable), ...getSummaryTableSizeInBytesColumn({ isILMAvailable, formatBytes }), @@ -264,6 +243,7 @@ export const getSummaryTableColumns = ({ ), sortable: true, truncateText: false, + width: '120px', }, ]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx index 720943b0d2bb8..24d57f927e6ea 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx @@ -14,7 +14,10 @@ import { getSummaryTableColumns } from './helpers'; 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 { TestProviders } from '../../mock/test_providers/test_providers'; +import { + TestDataQualityProviders, + TestExternalProviders, +} from '../../mock/test_providers/test_providers'; import { getSummaryTableItems } from '../pattern/helpers'; import { SortConfig } from '../../types'; import { Props, SummaryTable } from '.'; @@ -58,10 +61,7 @@ const items = getSummaryTableItems({ }); const defaultProps: Props = { - formatBytes, - formatNumber, getTableColumns: getSummaryTableColumns, - itemIdToExpandedRowMap: {}, items, pageIndex: 0, pageSize: 10, @@ -70,7 +70,27 @@ const defaultProps: Props = { setPageSize: jest.fn(), setSorting: jest.fn(), sorting: defaultSort, - toggleExpanded: jest.fn(), + onCheckNowAction: jest.fn(), + onExpandAction: jest.fn(), + checkState: Object.fromEntries( + indexNames.map((indexName) => [ + 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, + }, + ]) + ), }; describe('SummaryTable', () => { @@ -78,9 +98,11 @@ describe('SummaryTable', () => { jest.clearAllMocks(); render( - - - + + + + + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx index 6379539f05096..cc09109ea16f9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx @@ -9,31 +9,31 @@ import type { CriteriaWithPagination, EuiBasicTableColumn, Pagination } from '@e import { EuiInMemoryTable } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import type { IndexSummaryTableItem } from './helpers'; import { getShowPagination } from './helpers'; import { defaultSort, MIN_PAGE_SIZE } from '../pattern/helpers'; import { SortConfig } from '../../types'; import { useDataQualityContext } from '../data_quality_context'; +import { IndexSummaryTableItem } from '../pattern/types'; +import { UseIndicesCheckCheckState } from '../../use_indices_check/types'; export interface Props { - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; getTableColumns: ({ formatBytes, formatNumber, - itemIdToExpandedRowMap, + checkState, isILMAvailable, pattern, - toggleExpanded, + onExpandAction, + onCheckNowAction, }: { formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; - itemIdToExpandedRowMap: Record; + checkState: UseIndicesCheckCheckState; isILMAvailable: boolean; pattern: string; - toggleExpanded: (indexName: string) => void; + onExpandAction: (indexName: string) => void; + onCheckNowAction: (indexName: string) => void; }) => Array>; - itemIdToExpandedRowMap: Record; items: IndexSummaryTableItem[]; pageIndex: number; pageSize: number; @@ -42,14 +42,13 @@ export interface Props { setPageSize: (pageSize: number) => void; setSorting: (sortConfig: SortConfig) => void; sorting: SortConfig; - toggleExpanded: (indexName: string) => void; + onExpandAction: (indexName: string) => void; + onCheckNowAction: (indexName: string) => void; + checkState: UseIndicesCheckCheckState; } const SummaryTableComponent: React.FC = ({ - formatBytes, - formatNumber, getTableColumns, - itemIdToExpandedRowMap, items, pageIndex, pageSize, @@ -58,27 +57,31 @@ const SummaryTableComponent: React.FC = ({ setPageSize, setSorting, sorting, - toggleExpanded, + onExpandAction, + onCheckNowAction, + checkState, }) => { - const { isILMAvailable } = useDataQualityContext(); + const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); const columns = useMemo( () => getTableColumns({ formatBytes, formatNumber, - itemIdToExpandedRowMap, + checkState, isILMAvailable, pattern, - toggleExpanded, + onExpandAction, + onCheckNowAction, }), [ formatBytes, formatNumber, getTableColumns, + checkState, isILMAvailable, - itemIdToExpandedRowMap, + onCheckNowAction, + onExpandAction, pattern, - toggleExpanded, ] ); const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []); @@ -109,7 +112,6 @@ const SummaryTableComponent: React.FC = ({ columns={columns} data-test-subj="summaryTable" itemId={getItemId} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} items={items} onChange={onChange} pagination={ diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/translations.ts index 8d8e4adb9944f..7e005ba659c1d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/summary_table/translations.ts @@ -21,10 +21,10 @@ export const DOCS = i18n.translate( } ); -export const EXPAND = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandLabel', +export const VIEW_CHECK_DETAILS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.viewCheckDetailsLabel', { - defaultMessage: 'Expand', + defaultMessage: 'View check details', } ); @@ -124,3 +124,17 @@ export const THIS_INDEX_HAS_NOT_BEEN_CHECKED = i18n.translate( defaultMessage: 'This index has not been checked', } ); + +export const ACTIONS = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.actionsColumn', + { + defaultMessage: 'Actions', + } +); + +export const CHECK_INDEX = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.checkIndexButton', + { + defaultMessage: 'Check index', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/all_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/all_tab/index.tsx index 51988e631fa4d..c80d1f615f261 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/all_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/all_tab/index.tsx @@ -26,10 +26,10 @@ const AllTabComponent: React.FC = ({ indexName, partitionedFieldMetadata const title = useMemo(() => , []); return ( - <> +
{partitionedFieldMetadata.all.length > 0 ? ( <> - +

{i18n.ALL_CALLOUT(EcsVersion)}

@@ -48,7 +48,7 @@ const AllTabComponent: React.FC = ({ indexName, partitionedFieldMetadata titleSize="s" /> )} - +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.test.tsx index fc2ba0327062f..88bb4ef18ac47 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.test.tsx @@ -14,26 +14,18 @@ import { hostNameKeyword, someField, } from '../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../../mock/test_providers/test_providers'; import { CustomCallout } from '.'; -const content = 'you are reviewing a pull request'; - describe('CustomCallout', () => { beforeEach(() => { render( - - -
{content}
-
-
+ + + ); }); - test('it renders a title with the expected count of custom field mappings', () => { - expect(screen.getByTestId('title')).toHaveTextContent('2 Custom field mappings'); - }); - test('it includes the ECS version in the main content', () => { expect(screen.getByTestId('fieldsNotDefinedByEcs')).toHaveTextContent( `These fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.` @@ -43,8 +35,4 @@ describe('CustomCallout', () => { test('it notes ECS is a permissive schema', () => { expect(screen.getByTestId('ecsIsPermissive')).toHaveTextContent(ECS_IS_A_PERMISSIVE_SCHEMA); }); - - test('it renders the children', () => { - expect(screen.getByTestId('children')).toHaveTextContent(content); - }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.tsx index 8d1bc0c20a72c..f15e2a108a82c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/custom_callout/index.tsx @@ -8,34 +8,27 @@ import { EcsVersion } from '@elastic/ecs'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import type { CustomFieldMetadata } from '../../../../types'; import * as i18n from '../../../index_properties/translations'; interface Props { - children?: React.ReactNode; customFieldMetadata: CustomFieldMetadata[]; } -const CustomCalloutComponent: React.FC = ({ children, customFieldMetadata }) => { - const title = useMemo( - () => ( - {i18n.CUSTOM_CALLOUT_TITLE(customFieldMetadata.length)} - ), - [customFieldMetadata.length] - ); - +const CustomCalloutComponent: React.FC = ({ customFieldMetadata }) => { return ( - +
{i18n.CUSTOM_CALLOUT({ fieldCount: customFieldMetadata.length, version: EcsVersion })}
{i18n.ECS_IS_A_PERMISSIVE_SCHEMA}
- {children}
); }; +CustomCalloutComponent.displayName = 'CustomCalloutComponent'; + export const CustomCallout = React.memo(CustomCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.test.tsx index 590dff6d64a0b..7e1d1f9eb3c02 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.test.tsx @@ -14,47 +14,18 @@ import { MAPPINGS_THAT_CONFLICT_WITH_ECS, PAGES_MAY_NOT_DISPLAY_EVENTS, } from '../../../index_properties/translations'; -import { - eventCategory, - eventCategoryWithUnallowedValues, - hostNameWithTextMapping, - sourceIpWithTextMapping, -} from '../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; -import { EcsBasedFieldMetadata } from '../../../../types'; +import { TestExternalProviders } from '../../../../mock/test_providers/test_providers'; import { IncompatibleCallout } from '.'; -const content = 'Is your name Michael?'; - -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 - isEcsCompliant: false, // wildcard !== keyword -}; - describe('IncompatibleCallout', () => { beforeEach(() => { render( - - -
{content}
-
-
+ + + ); }); - test('it renders a title with the expected incompatible and family counts', () => { - expect(screen.getByTestId('title')).toHaveTextContent('4 incompatible fields'); - }); - 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}.` @@ -78,8 +49,4 @@ describe('IncompatibleCallout', () => { MAPPINGS_THAT_CONFLICT_WITH_ECS ); }); - - test('it renders the children', () => { - expect(screen.getByTestId('children')).toHaveTextContent(content); - }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.tsx index e1080569db4ee..6a692e05f8e54 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/incompatible_callout/index.tsx @@ -8,26 +8,14 @@ import { EcsVersion } from '@elastic/ecs'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import * as i18n from '../../../index_properties/translations'; import { CalloutItem } from '../../styles'; -import type { EcsBasedFieldMetadata } from '../../../../types'; - -interface Props { - children?: React.ReactNode; - ecsBasedFieldMetadata: EcsBasedFieldMetadata[]; -} - -const IncompatibleCalloutComponent: React.FC = ({ children, ecsBasedFieldMetadata }) => { - const fieldCount = ecsBasedFieldMetadata.length; - const title = useMemo( - () => {i18n.INCOMPATIBLE_CALLOUT_TITLE(fieldCount)}, - [fieldCount] - ); +const IncompatibleCalloutComponent: React.FC = () => { return ( - +
{i18n.INCOMPATIBLE_CALLOUT(EcsVersion)}
@@ -39,11 +27,10 @@ const IncompatibleCalloutComponent: React.FC = ({ children, ecsBasedField {i18n.MAPPINGS_THAT_CONFLICT_WITH_ECS} - - {children}
); }; + IncompatibleCalloutComponent.displayName = 'IncompatibleCalloutComponent'; export const IncompatibleCallout = React.memo(IncompatibleCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.test.tsx index 774b0a04ec174..d93b5dab41201 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.test.tsx @@ -10,29 +10,21 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { FIELDS_WITH_MAPPINGS_SAME_FAMILY } from '../../../index_properties/translations'; -import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../../../../mock/test_providers/test_providers'; import { SameFamilyCallout } from '.'; import { mockPartitionedFieldMetadataWithSameFamily } from '../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata_with_same_family'; -const content = 'you are reviewing a pull request'; - describe('SameFamilyCallout', () => { beforeEach(() => { render( - + -
{content}
-
-
+ /> + ); }); - test('it renders a title with the expected count of same family field mappings', () => { - expect(screen.getByTestId('title')).toHaveTextContent('1 Same family field mapping'); - }); - test('it includes the ECS version in the main content', () => { expect(screen.getByTestId('fieldsDefinedByEcs')).toHaveTextContent( `This field is defined by the Elastic Common Schema (ECS), version ${EcsVersion}, but its mapping type doesn't exactly match.` @@ -44,8 +36,4 @@ describe('SameFamilyCallout', () => { FIELDS_WITH_MAPPINGS_SAME_FAMILY ); }); - - test('it renders the children', () => { - expect(screen.getByTestId('children')).toHaveTextContent(content); - }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.tsx index ffec880687c8c..f89ebdda19889 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/callouts/same_family_callout/index.tsx @@ -7,28 +7,18 @@ import { EcsVersion } from '@elastic/ecs'; import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import * as i18n from '../../../index_properties/translations'; import type { EcsBasedFieldMetadata } from '../../../../types'; interface Props { - children?: React.ReactNode; ecsBasedFieldMetadata: EcsBasedFieldMetadata[]; } -const SameFamilyCalloutComponent: React.FC = ({ children, ecsBasedFieldMetadata }) => { - const title = useMemo( - () => ( - - {i18n.SAME_FAMILY_CALLOUT_TITLE(ecsBasedFieldMetadata.length)} - - ), - [ecsBasedFieldMetadata.length] - ); - +const SameFamilyCalloutComponent: React.FC = ({ ecsBasedFieldMetadata }) => { return ( - +
{i18n.SAME_FAMILY_CALLOUT({ fieldCount: ecsBasedFieldMetadata.length, @@ -41,9 +31,10 @@ const SameFamilyCalloutComponent: React.FC = ({ children, ecsBasedFieldMe {i18n.FIELDS_WITH_MAPPINGS_SAME_FAMILY}
- {children}
); }; +SameFamilyCalloutComponent.displayName = 'SameFamilyCalloutComponent'; + export const SameFamilyCallout = React.memo(SameFamilyCalloutComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.test.ts index 31dd9644bbc1d..424664c314832 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.test.ts @@ -7,12 +7,10 @@ import numeral from '@elastic/numeral'; import { EcsVersion } from '@elastic/ecs'; -import { euiThemeVars } from '@kbn/ui-theme'; import { ECS_IS_A_PERMISSIVE_SCHEMA } from '../../index_properties/translations'; import { getAllCustomMarkdownComments, - getCustomColor, getCustomMarkdownComment, showCustomCallout, } from './helpers'; @@ -21,7 +19,6 @@ import { someField, } from '../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { PartitionedFieldMetadata } from '../../../types'; import { EMPTY_STAT } from '../../../helpers'; const defaultBytesFormat = '0,0.[0]b'; @@ -55,21 +52,6 @@ ${ECS_IS_A_PERMISSIVE_SCHEMA} }); }); - describe('getCustomColor', () => { - test('it returns the expected color when there are custom fields', () => { - expect(getCustomColor(mockPartitionedFieldMetadata)).toEqual(euiThemeVars.euiColorLightShade); - }); - - test('it returns the expected color when custom fields is empty', () => { - const noCustomFields: PartitionedFieldMetadata = { - ...mockPartitionedFieldMetadata, - custom: [], // <-- empty - }; - - expect(getCustomColor(noCustomFields)).toEqual(euiThemeVars.euiTextColor); - }); - }); - describe('getAllCustomMarkdownComments', () => { test('it returns the expected comment', () => { expect( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts index f5b6ca7220809..55cb4898b7951 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts @@ -6,7 +6,6 @@ */ import { EcsVersion } from '@elastic/ecs'; -import { euiThemeVars } from '@kbn/ui-theme'; import { FIELD, INDEX_MAPPING_TYPE } from '../../../compare_fields_table/translations'; import { @@ -18,7 +17,6 @@ import { getSummaryTableMarkdownComment, } from '../../index_properties/markdown/helpers'; import * as i18n from '../../index_properties/translations'; -import { getFillColor } from '../summary_tab/helpers'; import type { CustomFieldMetadata, IlmPhase, PartitionedFieldMetadata } from '../../../types'; export const getCustomMarkdownComment = ({ @@ -40,11 +38,6 @@ ${i18n.ECS_IS_A_PERMISSIVE_SCHEMA} export const showCustomCallout = (customFieldMetadata: CustomFieldMetadata[]): boolean => customFieldMetadata.length > 0; -export const getCustomColor = (partitionedFieldMetadata: PartitionedFieldMetadata): string => - showCustomCallout(partitionedFieldMetadata.custom) - ? getFillColor('custom') - : euiThemeVars.euiTextColor; - export const getAllCustomMarkdownComments = ({ docsCount, formatBytes, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx index b976be6193087..8c786216bc55a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx @@ -5,15 +5,8 @@ * 2.0. */ -import { - copyToClipboard, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiEmptyPrompt, - EuiSpacer, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { CustomCallout } from '../callouts/custom_callout'; import { CompareFieldsTable } from '../../../compare_fields_table'; @@ -22,12 +15,11 @@ import { EmptyPromptBody } from '../../index_properties/empty_prompt_body'; import { EmptyPromptTitle } from '../../index_properties/empty_prompt_title'; import { getAllCustomMarkdownComments, showCustomCallout } from './helpers'; import * as i18n from '../../index_properties/translations'; -import { COPIED_RESULTS_TOAST_TITLE } from '../../../translations'; import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; import { useDataQualityContext } from '../../data_quality_context'; +import { StickyActions } from '../sticky_actions'; interface Props { - addSuccessToast: (toast: { title: string }) => void; docsCount: number; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; @@ -39,7 +31,6 @@ interface Props { } const CustomTabComponent: React.FC = ({ - addSuccessToast, docsCount, formatBytes, formatNumber, @@ -50,7 +41,7 @@ const CustomTabComponent: React.FC = ({ sizeInBytes, }) => { const { isILMAvailable } = useDataQualityContext(); - const markdownComments: string[] = useMemo( + const markdownComment: string = useMemo( () => getAllCustomMarkdownComments({ docsCount, @@ -62,7 +53,7 @@ const CustomTabComponent: React.FC = ({ partitionedFieldMetadata, patternDocsCount, sizeInBytes, - }), + }).join('\n'), [ docsCount, formatBytes, @@ -79,27 +70,11 @@ const CustomTabComponent: React.FC = ({ const body = useMemo(() => , []); const title = useMemo(() => , []); - const onCopy = useCallback(() => { - copyToClipboard(markdownComments.join('\n')); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, markdownComments]); - return ( - <> +
{showCustomCallout(partitionedFieldMetadata.custom) ? ( <> - - - - - {i18n.COPY_TO_CLIPBOARD} - - - - + @@ -108,11 +83,14 @@ const CustomTabComponent: React.FC = ({ getTableColumns={getCustomTableColumns} title={i18n.CUSTOM_FIELDS_TABLE_TITLE(indexName)} /> + + + ) : ( )} - +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx index 855ef75e80b84..39b932cd88e40 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/ecs_compliant_tab/index.tsx @@ -34,13 +34,10 @@ const EcsCompliantTabComponent: React.FC = ({ indexName, partitionedField const title = useMemo(() => , []); return ( - <> +
{!showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) ? ( <> - +

{i18n.ECS_COMPLIANT_CALLOUT({ fieldCount: partitionedFieldMetadata.ecsCompliant.length, @@ -71,7 +68,7 @@ const EcsCompliantTabComponent: React.FC = ({ indexName, partitionedField /> )} - +

); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx index 3c04a6473facf..3891a3d661561 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import { DARK_THEME } from '@elastic/charts'; -import { euiThemeVars } from '@kbn/ui-theme'; import { omit } from 'lodash/fp'; import { @@ -16,7 +14,7 @@ import { import { mockPartitionedFieldMetadata } from '../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; import { mockStatsAuditbeatIndex } from '../../mock/stats/mock_stats_packetbeat_index'; import { - getEcsCompliantColor, + getEcsCompliantBadgeColor, getMissingTimestampComment, getTabs, showMissingTimestampCallout, @@ -45,11 +43,9 @@ describe('helpers', () => { }); }); - describe('getEcsCompliantColor', () => { + describe('getEcsCompliantBadgeColor', () => { test('it returns the expected color for the ECS compliant data when the data includes an @timestamp', () => { - expect(getEcsCompliantColor(mockPartitionedFieldMetadata)).toEqual( - euiThemeVars.euiColorSuccess - ); + expect(getEcsCompliantBadgeColor(mockPartitionedFieldMetadata)).toBe('hollow'); }); test('it returns the expected color for the ECS compliant data does NOT includes an @timestamp', () => { @@ -60,7 +56,7 @@ describe('helpers', () => { ), }; - expect(getEcsCompliantColor(noTimestamp)).toEqual(euiThemeVars.euiColorDanger); + expect(getEcsCompliantBadgeColor(noTimestamp)).toEqual('danger'); }); }); @@ -68,28 +64,16 @@ describe('helpers', () => { test('it returns the expected tabs', () => { expect( getTabs({ - addSuccessToast: jest.fn(), - addToNewCaseDisabled: false, docsCount: 4, formatBytes: jest.fn(), formatNumber: jest.fn(), - getGroupByFieldsOnClick: jest.fn(), ilmPhase: 'unmanaged', indexName: 'auditbeat-custom-index-1', - isAssistantEnabled: true, - onAddToNewCase: jest.fn(), partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'auditbeat-*', patternDocsCount: 57410, - setSelectedTabId: jest.fn(), stats: mockStatsAuditbeatIndex, - baseTheme: DARK_THEME, }).map((x) => omit(['append', 'content'], x)) ).toEqual([ - { - id: 'summaryTab', - name: 'Summary', - }, { id: 'incompatibleTab', name: 'Incompatible fields', diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx index 8790ab12591b3..5220522350b07 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx @@ -5,39 +5,25 @@ * 2.0. */ -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; import { EuiBadge } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; import React from 'react'; +import styled from 'styled-components'; import { AllTab } from './all_tab'; import { CustomTab } from './custom_tab'; -import { getCustomColor } from './custom_tab/helpers'; import { EcsCompliantTab } from './ecs_compliant_tab'; -import { getSizeInBytes } from '../../helpers'; +import { getIncompatibleStatBadgeColor, getSizeInBytes } from '../../helpers'; import { IncompatibleTab } from './incompatible_tab'; -import { getIncompatibleColor, getSameFamilyColor } from './incompatible_tab/helpers'; import { ALL_TAB_ID, + CUSTOM_TAB_ID, ECS_COMPLIANT_TAB_ID, INCOMPATIBLE_TAB_ID, SAME_FAMILY_TAB_ID, - SUMMARY_TAB_ID, } from '../index_properties/helpers'; import { getMarkdownComment } from '../index_properties/markdown/helpers'; import * as i18n from '../index_properties/translations'; import { SameFamilyTab } from './same_family_tab'; -import { SummaryTab } from './summary_tab'; -import { getFillColor } from './summary_tab/helpers'; import type { EcsBasedFieldMetadata, IlmPhase, @@ -59,102 +45,50 @@ export const showMissingTimestampCallout = ( ecsBasedFieldMetadata: EcsBasedFieldMetadata[] ): boolean => !ecsBasedFieldMetadata.some((x) => x.name === '@timestamp'); -export const getEcsCompliantColor = (partitionedFieldMetadata: PartitionedFieldMetadata): string => - showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) - ? euiThemeVars.euiColorDanger - : getFillColor('ecs-compliant'); +export const getEcsCompliantBadgeColor = ( + partitionedFieldMetadata: PartitionedFieldMetadata +): string => + showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) ? 'danger' : 'hollow'; + +const StyledBadge = styled(EuiBadge)` + text-align: right; + cursor: pointer; +`; export const getTabs = ({ - addSuccessToast, - addToNewCaseDisabled, docsCount, formatBytes, formatNumber, - getGroupByFieldsOnClick, ilmPhase, indexName, - isAssistantEnabled, - onAddToNewCase, partitionedFieldMetadata, - pattern, patternDocsCount, - setSelectedTabId, stats, - theme, - baseTheme, }: { - addSuccessToast: (toast: { title: string }) => void; - addToNewCaseDisabled: boolean; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; docsCount: number; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; ilmPhase: IlmPhase | undefined; indexName: string; - isAssistantEnabled: boolean; - onAddToNewCase: (markdownComments: string[]) => void; partitionedFieldMetadata: PartitionedFieldMetadata; - pattern: string; patternDocsCount: number; - setSelectedTabId: (tabId: string) => void; stats: Record | null; - theme?: PartialTheme; - baseTheme: Theme; }) => [ - { - content: ( - - ), - id: SUMMARY_TAB_ID, - name: i18n.SUMMARY, - }, { append: ( - + {partitionedFieldMetadata.incompatible.length} - + ), content: ( {partitionedFieldMetadata.sameFamily.length} - ), + append: {partitionedFieldMetadata.sameFamily.length}, content: ( - {partitionedFieldMetadata.custom.length} - - ), + append: {partitionedFieldMetadata.custom.length}, content: ( ), - id: 'customTab', + id: CUSTOM_TAB_ID, name: i18n.CUSTOM_FIELDS, }, { append: ( - + {partitionedFieldMetadata.ecsCompliant.length} - + ), content: ( @@ -218,11 +144,7 @@ export const getTabs = ({ name: i18n.ECS_COMPLIANT_FIELDS, }, { - append: ( - - {partitionedFieldMetadata.all.length} - - ), + 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/data_quality_panel/tabs/incompatible_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts index 34f4bd2448686..9851da18072a7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts @@ -7,18 +7,15 @@ import numeral from '@elastic/numeral'; import { EcsVersion } from '@elastic/ecs'; -import { euiThemeVars } from '@kbn/ui-theme'; import { getAllIncompatibleMarkdownComments, - getIncompatibleColor, getIncompatibleFieldsMarkdownComment, getIncompatibleFieldsMarkdownTablesComment, getIncompatibleMappings, getIncompatibleMappingsFields, getIncompatibleValues, getIncompatibleValuesFields, - getSameFamilyColor, showInvalidCallout, } from './helpers'; import { EMPTY_STAT } from '../../../helpers'; @@ -54,18 +51,6 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS} }); }); - describe('getIncompatibleColor', () => { - test('it returns the expected color', () => { - expect(getIncompatibleColor()).toEqual(euiThemeVars.euiColorDanger); - }); - }); - - describe('getSameFamilyColor', () => { - test('it returns the expected color', () => { - expect(getSameFamilyColor()).toEqual(euiThemeVars.euiColorLightShade); - }); - }); - describe('getIncompatibleMappings', () => { test('it (only) returns the mappings where type !== indexFieldType', () => { expect(getIncompatibleMappings(mockPartitionedFieldMetadata.incompatible)).toEqual([ diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts index 857b53589f163..c4c6dfd4a2a82 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts @@ -17,7 +17,6 @@ import { getTabCountsMarkdownComment, escape, } from '../../index_properties/markdown/helpers'; -import { getFillColor } from '../summary_tab/helpers'; import * as i18n from '../../index_properties/translations'; import type { EcsBasedFieldMetadata, IlmPhase, PartitionedFieldMetadata } from '../../../types'; import { @@ -47,10 +46,6 @@ ${i18n.MAPPINGS_THAT_CONFLICT_WITH_ECS} export const showInvalidCallout = (ecsBasedFieldMetadata: EcsBasedFieldMetadata[]): boolean => ecsBasedFieldMetadata.length > 0; -export const getIncompatibleColor = (): string => getFillColor('incompatible'); - -export const getSameFamilyColor = (): string => getFillColor('same-family'); - export const getIncompatibleMappings = ( ecsBasedFieldMetadata: EcsBasedFieldMetadata[] ): EcsBasedFieldMetadata[] => diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx index 94333d57da0b2..52559b3d5116c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx @@ -5,16 +5,8 @@ * 2.0. */ -import { NewChat } from '@kbn/elastic-assistant'; -import { - copyToClipboard, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiEmptyPrompt, - EuiSpacer, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { IncompatibleCallout } from '../callouts/incompatible_callout'; import { CompareFieldsTable } from '../../../compare_fields_table'; @@ -29,51 +21,35 @@ import { showInvalidCallout, } from './helpers'; import * as i18n from '../../index_properties/translations'; -import { CopyToClipboardButton } from '../styles'; import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE, INCOMPATIBLE_FIELD_VALUES_TABLE_TITLE, } from './translations'; -import { - COPIED_RESULTS_TOAST_TITLE, - DATA_QUALITY_PROMPT_CONTEXT_PILL, - DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP, - DATA_QUALITY_SUGGESTED_USER_PROMPT, -} from '../../../translations'; import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; -import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '../summary_tab/callout_summary/translations'; import { useDataQualityContext } from '../../data_quality_context'; +import { StickyActions } from '../sticky_actions'; interface Props { - addSuccessToast: (toast: { title: string }) => void; - addToNewCaseDisabled: boolean; docsCount: number; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; - isAssistantEnabled: boolean; - onAddToNewCase: (markdownComments: string[]) => void; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; sizeInBytes: number | undefined; } const IncompatibleTabComponent: React.FC = ({ - addSuccessToast, - addToNewCaseDisabled, docsCount, formatBytes, formatNumber, ilmPhase, indexName, - isAssistantEnabled, - onAddToNewCase, partitionedFieldMetadata, patternDocsCount, sizeInBytes, }) => { - const { isILMAvailable } = useDataQualityContext(); const body = useMemo(() => , []); const title = useMemo(() => , []); const incompatibleMappings = useMemo( @@ -84,7 +60,10 @@ const IncompatibleTabComponent: React.FC = ({ () => getIncompatibleValues(partitionedFieldMetadata.incompatible), [partitionedFieldMetadata.incompatible] ); - const markdownComments: string[] = useMemo( + + const { isILMAvailable } = useDataQualityContext(); + + const markdownComment: string = useMemo( () => getAllIncompatibleMarkdownComments({ docsCount, @@ -96,7 +75,7 @@ const IncompatibleTabComponent: React.FC = ({ partitionedFieldMetadata, patternDocsCount, sizeInBytes, - }), + }).join('\n'), [ docsCount, formatBytes, @@ -109,55 +88,12 @@ const IncompatibleTabComponent: React.FC = ({ sizeInBytes, ] ); - const onClickAddToCase = useCallback( - () => onAddToNewCase([markdownComments.join('\n')]), - [markdownComments, onAddToNewCase] - ); - const onCopy = useCallback(() => { - copyToClipboard(markdownComments.join('\n')); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, markdownComments]); - - const getPromptContext = useCallback(async () => markdownComments.join('\n'), [markdownComments]); return ( -
+
{showInvalidCallout(partitionedFieldMetadata.incompatible) ? ( <> - - - - - {i18n.ADD_TO_NEW_CASE} - - - - - - {i18n.COPY_TO_CLIPBOARD} - - - - - - - - + <> {incompatibleMappings.length > 0 && ( @@ -186,6 +122,15 @@ const IncompatibleTabComponent: React.FC = ({ )} + + + ) : ( void; docsCount: number; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; @@ -31,7 +29,6 @@ interface Props { } const SameFamilyTabComponent: React.FC = ({ - addSuccessToast, docsCount, formatBytes, formatNumber, @@ -47,7 +44,7 @@ const SameFamilyTabComponent: React.FC = ({ ); const { isILMAvailable } = useDataQualityContext(); - const markdownComments: string[] = useMemo( + const markdownComment: string = useMemo( () => getAllSameFamilyMarkdownComments({ docsCount, @@ -59,7 +56,7 @@ const SameFamilyTabComponent: React.FC = ({ partitionedFieldMetadata, patternDocsCount, sizeInBytes, - }), + }).join('\n'), [ docsCount, formatBytes, @@ -73,21 +70,9 @@ const SameFamilyTabComponent: React.FC = ({ ] ); - const onCopy = useCallback(() => { - copyToClipboard(markdownComments.join('\n')); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, markdownComments]); - return ( -
- - - {i18n.COPY_TO_CLIPBOARD} - - +
+ <> {sameFamilyMappings.length > 0 && ( @@ -102,6 +87,9 @@ const SameFamilyTabComponent: React.FC = ({ )} + + 0 ? 'm' : 'l'} /> +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/sticky_actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/sticky_actions/index.tsx new file mode 100644 index 0000000000000..57b17a7453dd0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/sticky_actions/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import styled from 'styled-components'; +import { Actions } from '../../actions'; + +export const CopyToClipboardButton = styled(EuiButtonEmpty)` + margin-left: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +interface Props { + markdownComment: string; + showAddToNewCaseAction?: boolean; + showCopyToClipboardAction?: boolean; + showChatAction?: boolean; + indexName?: string; +} + +const StyledStickyContainer = styled.div` + padding: ${({ theme }) => theme.eui.euiSizeL} 0; + background: ${({ theme }) => theme.eui.euiColorEmptyShade}; + position: sticky; + bottom: 0; + left: 0; + right: 0; + border-top: 1px solid ${({ theme }) => theme.eui.euiBorderColor}; +`; + +const StickyActionsComponent: FC = ({ + indexName, + markdownComment, + showCopyToClipboardAction, + showAddToNewCaseAction, + showChatAction, +}) => { + return ( + + + + ); +}; + +StickyActionsComponent.displayName = 'StickyActionsComponent'; + +export const StickyActions = React.memo(StickyActionsComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx index 2714d1002c40c..20976b1684003 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; export const DEFAULT_LEGEND_HEIGHT = 300; // px @@ -23,10 +23,6 @@ export const ChartFlexItem = styled(EuiFlexItem)<{ min-height: ${({ $minChartHeight }) => `${$minChartHeight}px`}; `; -export const CopyToClipboardButton = styled(EuiButtonEmpty)` - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; - export const LegendContainer = styled.div<{ $height?: number; $width?: number; @@ -37,11 +33,3 @@ export const LegendContainer = styled.div<{ scrollbar-width: thin; ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} `; - -export const StorageTreemapContainer = styled.div` - padding: ${({ theme }) => theme.eui.euiSizeM}; -`; - -export const ChartLegendLink = styled(EuiLink)` - width: 100%; -`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx deleted file mode 100644 index 96b110a05807e..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { NewChat } from '@kbn/elastic-assistant'; -import { copyToClipboard, EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; - -import { MissingTimestampCallout } from '../../callouts/missing_timestamp_callout'; -import { IncompatibleCallout } from '../../callouts/incompatible_callout'; -import { showMissingTimestampCallout } from '../../helpers'; -import { getMarkdownComments } from '../helpers'; -import { showInvalidCallout } from '../../incompatible_tab/helpers'; -import { CopyToClipboardButton } from '../../styles'; -import * as i18n from '../../../index_properties/translations'; -import { - COPIED_RESULTS_TOAST_TITLE, - DATA_QUALITY_PROMPT_CONTEXT_PILL, - DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP, - DATA_QUALITY_SUGGESTED_USER_PROMPT, -} from '../../../../translations'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../../types'; -import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from './translations'; -import { useDataQualityContext } from '../../../data_quality_context'; - -interface Props { - addSuccessToast: (toast: { title: string }) => void; - addToNewCaseDisabled: boolean; - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - ilmPhase: IlmPhase | undefined; - indexName: string; - isAssistantEnabled: boolean; - onAddToNewCase: (markdownComment: string[]) => void; - partitionedFieldMetadata: PartitionedFieldMetadata; - pattern: string; - patternDocsCount: number; - sizeInBytes: number | undefined; -} - -const CalloutSummaryComponent: React.FC = ({ - addSuccessToast, - addToNewCaseDisabled, - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isAssistantEnabled, - onAddToNewCase, - partitionedFieldMetadata, - pattern, - patternDocsCount, - sizeInBytes, -}) => { - const { isILMAvailable } = useDataQualityContext(); - const markdownComments: string[] = useMemo( - () => - getMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - pattern, - patternDocsCount, - sizeInBytes, - }), - [ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - pattern, - patternDocsCount, - sizeInBytes, - ] - ); - - const onClickAddToCase = useCallback( - () => onAddToNewCase([markdownComments.join('\n')]), - [markdownComments, onAddToNewCase] - ); - - const onCopy = useCallback(() => { - copyToClipboard(markdownComments.join('\n')); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, markdownComments]); - - const getPromptContext = useCallback(async () => markdownComments.join('\n'), [markdownComments]); - - const showActions = - showInvalidCallout(partitionedFieldMetadata.incompatible) || - showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant); - - return ( - <> - {showInvalidCallout(partitionedFieldMetadata.incompatible) && ( - <> - - - - )} - {showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) && ( - <> - - - - )} - {showActions && ( - <> - - - - {i18n.ADD_TO_NEW_CASE} - - - - - - {i18n.COPY_TO_CLIPBOARD} - - - - - - - - - - - )} - - ); -}; - -CalloutSummaryComponent.displayName = 'CalloutSummaryComponent'; - -export const CalloutSummary = React.memo(CalloutSummaryComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts deleted file mode 100644 index c89cf1d5158ec..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts +++ /dev/null @@ -1,255 +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 { euiThemeVars } from '@kbn/ui-theme'; -import { EMPTY_STAT } from '../../../helpers'; - -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 { PartitionedFieldMetadata } from '../../../types'; -import { - ALL_TAB_ID, - CUSTOM_TAB_ID, - ECS_COMPLIANT_TAB_ID, - INCOMPATIBLE_TAB_ID, - SAME_FAMILY_TAB_ID, -} from '../../index_properties/helpers'; -import { - CUSTOM_FIELDS, - ECS_COMPLIANT_FIELDS, - INCOMPATIBLE_FIELDS, - UNKNOWN, -} from '../../index_properties/translations'; -import { SAME_FAMILY } from '../../stat_label/translations'; -import { - CategoryId, - getFillColor, - getMarkdownComments, - getNodeLabel, - getSummaryData, - getTabId, -} from './helpers'; -import { EcsFlatTyped } from '../../../constants'; - -describe('helpers', () => { - describe('getSummaryData', () => { - test('it returns the expected `SummaryData`', () => { - expect(getSummaryData(mockPartitionedFieldMetadataWithSameFamily)).toEqual([ - { categoryId: 'incompatible', mappings: 3 }, - { categoryId: 'custom', mappings: 4 }, - { categoryId: 'ecs-compliant', mappings: 2 }, - { categoryId: 'same-family', mappings: 1 }, - ]); - }); - }); - - describe('getFillColor', () => { - const invalid: CategoryId = 'invalid-category-id' as CategoryId; - - const categories: Array<{ - categoryId: CategoryId; - expectedColor: string; - }> = [ - { - categoryId: 'incompatible', - expectedColor: euiThemeVars.euiColorDanger, - }, - { - categoryId: 'custom', - expectedColor: euiThemeVars.euiColorLightShade, - }, - { - categoryId: 'ecs-compliant', - expectedColor: euiThemeVars.euiColorSuccess, - }, - { - categoryId: 'same-family', - expectedColor: euiThemeVars.euiColorLightShade, - }, - { - categoryId: invalid, - expectedColor: euiThemeVars.euiColorGhost, - }, - ]; - - categories.forEach(({ categoryId, expectedColor }) => { - test(`it returns the expected color for category '${categoryId}'`, () => { - expect(getFillColor(categoryId)).toEqual(expectedColor); - }); - }); - }); - - describe('getNodeLabel', () => { - const invalid: CategoryId = 'invalid-category-id' as CategoryId; - - const categories: Array<{ - categoryId: CategoryId; - expectedLabel: string; - }> = [ - { - categoryId: 'incompatible', - expectedLabel: INCOMPATIBLE_FIELDS, - }, - { - categoryId: 'custom', - expectedLabel: CUSTOM_FIELDS, - }, - { - categoryId: 'ecs-compliant', - expectedLabel: ECS_COMPLIANT_FIELDS, - }, - { - categoryId: 'same-family', - expectedLabel: SAME_FAMILY, - }, - { - categoryId: invalid, - expectedLabel: UNKNOWN, - }, - ]; - - categories.forEach(({ categoryId, expectedLabel }) => { - test(`it returns the expected label for category '${categoryId}'`, () => { - expect(getNodeLabel(categoryId)).toEqual(expectedLabel); - }); - }); - }); - - describe('getTabId', () => { - const groupByFields: Array<{ - groupByField: string; - expectedTabId: string; - }> = [ - { - groupByField: 'incompatible', - expectedTabId: INCOMPATIBLE_TAB_ID, - }, - { - groupByField: 'custom', - expectedTabId: CUSTOM_TAB_ID, - }, - { - groupByField: 'ecs-compliant', - expectedTabId: ECS_COMPLIANT_TAB_ID, - }, - { - groupByField: 'same-family', - expectedTabId: SAME_FAMILY_TAB_ID, - }, - { - groupByField: 'some-other-group', - expectedTabId: ALL_TAB_ID, - }, - ]; - - groupByFields.forEach(({ groupByField, expectedTabId }) => { - test(`it returns the expected tab ID for groupByField '${groupByField}'`, () => { - expect(getTabId(groupByField)).toEqual(expectedTabId); - }); - }); - }); - - describe('getMarkdownComments', () => { - 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 isILMAvailable = true; - - test('it returns the expected comment when the index has incompatible fields ', () => { - expect( - getMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable, - partitionedFieldMetadata: mockPartitionedFieldMetadata, - pattern: 'auditbeat-*', - 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 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 - 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 an empty array when the index does NOT have incompatible fields ', () => { - const noIncompatible: PartitionedFieldMetadata = { - ...mockPartitionedFieldMetadata, - incompatible: [], // <-- no incompatible fields - }; - - expect( - getMarkdownComments({ - docsCount: 4, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-index-1', - isILMAvailable, - partitionedFieldMetadata: noIncompatible, - pattern: 'auditbeat-*', - patternDocsCount: 57410, - sizeInBytes: 28413, - }) - ).toEqual([]); - }); - - test('it returns a missing timestamp comment for an empty index', () => { - const emptyIndex: PartitionedFieldMetadata = { - all: [], - ecsCompliant: [], - custom: [], - incompatible: [ - { - ...EcsFlatTyped['@timestamp'], - hasEcsMetadata: true, - indexFieldName: '@timestamp', - indexFieldType: '-', - indexInvalidValues: [], - isEcsCompliant: false, - isInSameFamily: false, - }, - ], - sameFamily: [], - }; - - expect( - getMarkdownComments({ - docsCount: 0, - formatBytes, - formatNumber, - ilmPhase: 'unmanaged', - indexName: 'auditbeat-custom-empty-index-1', - isILMAvailable, - partitionedFieldMetadata: emptyIndex, - pattern: 'auditbeat-*', - patternDocsCount: 57410, - sizeInBytes: 247, - }) - ).toEqual([ - '### auditbeat-custom-empty-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-empty-index-1 | 0 (0.0%) | 1 | `unmanaged` | 247B |\n\n', - '### **Incompatible fields** `1` **Same family** `0` **Custom fields** `0` **ECS compliant fields** `0` **All fields** `0`\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 ${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 - auditbeat-custom-empty-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| @timestamp | `date` | `-` |\n\n\n', - '#### 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', - ]); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts deleted file mode 100644 index 001fcbdcda779..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiThemeVars } from '@kbn/ui-theme'; - -import { getMissingTimestampComment, showMissingTimestampCallout } from '../helpers'; -import { - ALL_TAB_ID, - ECS_COMPLIANT_TAB_ID, - CUSTOM_TAB_ID, - INCOMPATIBLE_TAB_ID, - SAME_FAMILY_TAB_ID, -} from '../../index_properties/helpers'; -import { - getAllIncompatibleMarkdownComments, - showInvalidCallout, -} from '../incompatible_tab/helpers'; -import * as i18n from '../../index_properties/translations'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; - -export type CategoryId = 'incompatible' | 'custom' | 'ecs-compliant' | 'same-family'; - -interface SummaryData { - categoryId: CategoryId; - mappings: number; -} - -export const getSummaryData = ( - partitionedFieldMetadata: PartitionedFieldMetadata -): SummaryData[] => [ - { categoryId: 'incompatible', mappings: partitionedFieldMetadata.incompatible.length }, - { categoryId: 'custom', mappings: partitionedFieldMetadata.custom.length }, - { categoryId: 'ecs-compliant', mappings: partitionedFieldMetadata.ecsCompliant.length }, - { categoryId: 'same-family', mappings: partitionedFieldMetadata.sameFamily.length }, -]; - -export const getFillColor = (categoryId: CategoryId | string): string => { - switch (categoryId) { - case 'incompatible': - return euiThemeVars.euiColorDanger; - case 'same-family': - return euiThemeVars.euiColorLightShade; - case 'custom': - return euiThemeVars.euiColorLightShade; - case 'ecs-compliant': - return euiThemeVars.euiColorSuccess; - default: - return euiThemeVars.euiColorGhost; - } -}; - -export const getNodeLabel = (categoryId: CategoryId): string => { - switch (categoryId) { - case 'incompatible': - return i18n.INCOMPATIBLE_FIELDS; - case 'same-family': - return i18n.SAME_FAMILY; - case 'custom': - return i18n.CUSTOM_FIELDS; - case 'ecs-compliant': - return i18n.ECS_COMPLIANT_FIELDS; - default: - return i18n.UNKNOWN; - } -}; - -export const getTabId = (groupByField: string): string => { - switch (groupByField) { - case 'incompatible': - return INCOMPATIBLE_TAB_ID; - case 'same-family': - return SAME_FAMILY_TAB_ID; - case 'custom': - return CUSTOM_TAB_ID; - case 'ecs-compliant': - return ECS_COMPLIANT_TAB_ID; - default: - return ALL_TAB_ID; - } -}; - -const isString = (x: string | null): x is string => typeof x === 'string'; - -export const getMarkdownComments = ({ - 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; - pattern: string; - patternDocsCount: number; - sizeInBytes: number | undefined; -}): string[] => { - const invalidMarkdownComments = showInvalidCallout(partitionedFieldMetadata.incompatible) - ? getAllIncompatibleMarkdownComments({ - docsCount, - formatBytes, - formatNumber, - ilmPhase, - indexName, - isILMAvailable, - partitionedFieldMetadata, - patternDocsCount, - sizeInBytes, - }) - : []; - - const showMissingTimestampComment = showMissingTimestampCallout( - partitionedFieldMetadata.ecsCompliant - ) - ? getMissingTimestampComment() - : null; - - return [...invalidMarkdownComments, showMissingTimestampComment].filter(isString); -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx deleted file mode 100644 index eed0c5a69d3b3..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx +++ /dev/null @@ -1,106 +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 type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { CalloutSummary } from './callout_summary'; -import { EcsSummaryDonutChart } from '../../../ecs_summary_donut_chart'; -import { ALL_TAB_ID } from '../../index_properties/helpers'; -import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; - -interface Props { - addSuccessToast: (toast: { title: string }) => void; - addToNewCaseDisabled: boolean; - docsCount: number; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - ilmPhase: IlmPhase | undefined; - indexName: string; - isAssistantEnabled: boolean; - onAddToNewCase: (markdownComments: string[]) => void; - partitionedFieldMetadata: PartitionedFieldMetadata; - pattern: string; - patternDocsCount: number; - setSelectedTabId: (tabId: string) => void; - sizeInBytes: number | undefined; - theme?: PartialTheme; - baseTheme: Theme; -} - -const SummaryTabComponent: React.FC = ({ - addSuccessToast, - addToNewCaseDisabled, - formatBytes, - formatNumber, - docsCount, - getGroupByFieldsOnClick, - ilmPhase, - indexName, - isAssistantEnabled, - onAddToNewCase, - partitionedFieldMetadata, - pattern, - patternDocsCount, - setSelectedTabId, - sizeInBytes, - theme, - baseTheme, -}) => ( - <> - - - - -); - -SummaryTabComponent.displayName = 'SummaryTabComponent'; - -export const SummaryTab = React.memo(SummaryTabComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx deleted file mode 100644 index 78f22eccfd3d2..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx +++ /dev/null @@ -1,95 +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, { useCallback } from 'react'; - -import { ChartLegendItem } from './chart_legend_item'; -import { getEcsCompliantColor } from '../../data_quality_panel/tabs/helpers'; -import { - ECS_COMPLIANT_TAB_ID, - CUSTOM_TAB_ID, - INCOMPATIBLE_TAB_ID, - SAME_FAMILY_TAB_ID, -} from '../../data_quality_panel/index_properties/helpers'; -import { getCustomColor } from '../../data_quality_panel/tabs/custom_tab/helpers'; -import { - getIncompatibleColor, - getSameFamilyColor, -} from '../../data_quality_panel/tabs/incompatible_tab/helpers'; -import type { PartitionedFieldMetadata } from '../../types'; -import * as i18n from '../../data_quality_panel/index_properties/translations'; -import { LegendContainer } from '../../data_quality_panel/tabs/styles'; - -const LEGEND_WIDTH = 200; // px - -interface Props { - partitionedFieldMetadata: PartitionedFieldMetadata; - setSelectedTabId: (tabId: string) => void; -} - -const ChartLegendComponent: React.FC = ({ partitionedFieldMetadata, setSelectedTabId }) => { - const showIncompatibleTab = useCallback( - () => setSelectedTabId(INCOMPATIBLE_TAB_ID), - [setSelectedTabId] - ); - - const showSameFamilyTab = useCallback( - () => setSelectedTabId(SAME_FAMILY_TAB_ID), - [setSelectedTabId] - ); - - const showCustomTab = useCallback(() => setSelectedTabId(CUSTOM_TAB_ID), [setSelectedTabId]); - - const showEcsCompliantTab = useCallback( - () => setSelectedTabId(ECS_COMPLIANT_TAB_ID), - [setSelectedTabId] - ); - - return ( - - {partitionedFieldMetadata.incompatible.length > 0 && ( - - )} - - {partitionedFieldMetadata.sameFamily.length > 0 && ( - - )} - - {partitionedFieldMetadata.custom.length > 0 && ( - - )} - - {partitionedFieldMetadata.ecsCompliant.length > 0 && ( - - )} - - ); -}; - -ChartLegendComponent.displayName = 'ChartLegendComponent'; - -export const ChartLegend = React.memo(ChartLegendComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts deleted file mode 100644 index 1ab8c650940d4..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { allMetadataIsEmpty } from './helpers'; -import { mockPartitionedFieldMetadata } from '../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; -import { PartitionedFieldMetadata } from '../types'; - -describe('helpers', () => { - describe('allMetadataIsEmpty', () => { - test('it returns false when `all` is NOT is empty', () => { - expect(allMetadataIsEmpty(mockPartitionedFieldMetadata)).toBe(false); - }); - - test('it returns true when `all` is is empty', () => { - const allIsEmpty: PartitionedFieldMetadata = { - all: [], // <-- empty - custom: [], - ecsCompliant: [], - incompatible: [], - sameFamily: [], - }; - - expect(allMetadataIsEmpty(allIsEmpty)).toBe(true); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/index.tsx deleted file mode 100644 index bb5d274d0f825..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/index.tsx +++ /dev/null @@ -1,203 +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 { - Datum, - ElementClickListener, - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, - PartitionLayer, -} from '@elastic/charts'; -import { Chart, Partition, PartitionLayout, Settings } from '@elastic/charts'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { i18n } from '@kbn/i18n'; -import { ChartLegend } from './chart_legend'; -import { - getFillColor, - getNodeLabel, - getSummaryData, - getTabId, -} from '../data_quality_panel/tabs/summary_tab/helpers'; -import { allMetadataIsEmpty } from './helpers'; -import * as translations from './translations'; -import type { PartitionedFieldMetadata } from '../types'; - -export const DEFAULT_HEIGHT = 180; // px - -const DonutTextWrapper = styled(EuiFlexGroup)` - max-width: 77px; - position: absolute; - top: 40%; - width: 100%; - z-index: 1; -`; - -const CenteredFlexItem = styled(EuiFlexItem)` - align-items: center; - position: relative; -`; - -const donutTheme: PartialTheme = { - chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, - partition: { - idealFontSizeJump: 1.1, - outerSizeRatio: 1, - emptySizeRatio: 0.8, - circlePadding: 4, - }, -}; - -interface Props { - defaultTabId: string; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; - height?: number; - partitionedFieldMetadata: PartitionedFieldMetadata; - setSelectedTabId: (tabId: string) => void; - theme?: PartialTheme; - baseTheme: Theme; -} - -const EcsSummaryDonutChartComponent: React.FC = ({ - defaultTabId, - getGroupByFieldsOnClick, - height = DEFAULT_HEIGHT, - partitionedFieldMetadata, - setSelectedTabId, - theme = {}, - baseTheme, -}) => { - const summaryData = useMemo( - () => getSummaryData(partitionedFieldMetadata), - [partitionedFieldMetadata] - ); - const valueAccessor = useCallback((d: Datum) => d.mappings as number, []); - const valueFormatter = useCallback((d: number) => `${d}`, []); - const layers = useMemo( - (): PartitionLayer[] => [ - { - groupByRollup: (d: Datum) => d.categoryId, - nodeLabel: (d: Datum) => getNodeLabel(d), - shape: { - fillColor: getFillColor, - }, - }, - ], - [] - ); - const showDefaultTab = useCallback( - () => setSelectedTabId(defaultTabId), - [defaultTabId, setSelectedTabId] - ); - const onElementClick: ElementClickListener = useCallback( - (event) => { - const { groupByField0 } = getGroupByFieldsOnClick(event); - - setSelectedTabId(getTabId(groupByField0)); - }, - [getGroupByFieldsOnClick, setSelectedTabId] - ); - - if (allMetadataIsEmpty(partitionedFieldMetadata)) { - return null; - } - - return ( - <> - -

{translations.CHART_TITLE}

-
- - - - - - - - - {partitionedFieldMetadata.all.length} - - {translations.FIELDS} - - - - - - - - - - - - - - - - - ); -}; - -EcsSummaryDonutChartComponent.displayName = 'EcsSummaryDonutChartComponent'; - -export const EcsSummaryDonutChart = React.memo(EcsSummaryDonutChartComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/translations.ts deleted file mode 100644 index 4df2f41cfdbd3..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ecs_summary_donut_chart/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const CHART_TITLE = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.chartTitle', - { - defaultMessage: 'Field mappings', - } -); - -export const FIELDS = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.fieldsLabel', - { - defaultMessage: 'Fields', - } -); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.test.ts index bfe3ffc0c5e55..0058e2436a526 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.test.ts @@ -6,7 +6,6 @@ */ import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; -import { euiThemeVars } from '@kbn/ui-theme'; import { omit } from 'lodash/fp'; import { @@ -19,13 +18,11 @@ import { getFieldTypes, getPatternIlmPhaseDescription, getIlmPhaseDescription, - getIncompatibleStatColor, getIndexNames, getIsInSameFamily, getMissingTimestampFieldMetadata, getPartitionedFieldMetadata, getPartitionedFieldMetadataStats, - getSameFamilyStatColor, getSizeInBytes, getTotalDocsCount, getTotalPatternIncompatible, @@ -161,32 +158,6 @@ describe('helpers', () => { }); }); - describe('getSameFamilyStatColor', () => { - it('returns the expected color when sameFamily is greater than zero', () => { - const result = getSameFamilyStatColor(1); - - expect(result).toEqual(euiThemeVars.euiColorLightShade); - }); - - it('returns undefined when sameFamily is 0', () => { - const result = getSameFamilyStatColor(0); - - expect(result).toBeUndefined(); - }); - - it('returns undefined when sameFamily is negative', () => { - const result = getSameFamilyStatColor(-1); - - expect(result).toBeUndefined(); - }); - - it('returns undefined when sameFamily is undefined', () => { - const result = getSameFamilyStatColor(undefined); - - expect(result).toBeUndefined(); - }); - }); - describe('getIndexNames', () => { const isILMAvailable = true; const ilmPhases = ['hot', 'warm', 'unmanaged']; @@ -1208,26 +1179,6 @@ describe('helpers', () => { }); }); - describe('getIncompatibleStatColor', () => { - test('it returns the expected color when incompatible is greater than zero', () => { - const incompatible = 123; - - expect(getIncompatibleStatColor(incompatible)).toBe('#bd271e'); - }); - - test('it returns undefined when incompatible is zero', () => { - const incompatible = 0; - - expect(getIncompatibleStatColor(incompatible)).toBeUndefined(); - }); - - test('it returns undefined when incompatible is undefined', () => { - const incompatible = undefined; - - expect(getIncompatibleStatColor(incompatible)).toBeUndefined(); - }); - }); - describe('getErrorSummary', () => { test('it returns the expected error summary', () => { const resultWithError: DataQualityCheckResult = { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts index 0980ac3763347..9a56e00ba547a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts @@ -10,7 +10,6 @@ import type { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch import { has, sortBy } from 'lodash/fp'; import { IToasts } from '@kbn/core-notifications-browser'; import { getIlmPhase } from './data_quality_panel/pattern/helpers'; -import { getFillColor } from './data_quality_panel/tabs/summary_tab/helpers'; import * as i18n from './translations'; @@ -34,7 +33,6 @@ import { EcsFlatTyped } from './constants'; const EMPTY_INDEX_NAMES: string[] = []; export const INTERNAL_API_VERSION = '1'; - export const getIndexNames = ({ ilmExplain, ilmPhases, @@ -401,11 +399,8 @@ export const getTotalPatternSameFamily = ( return allResults.reduce((acc, { sameFamily }) => acc + (sameFamily ?? 0), 0); }; -export const getIncompatibleStatColor = (incompatible: number | undefined): string | undefined => - incompatible != null && incompatible > 0 ? getFillColor('incompatible') : undefined; - -export const getSameFamilyStatColor = (sameFamily: number | undefined): string | undefined => - sameFamily != null && sameFamily > 0 ? getFillColor('same-family') : undefined; +export const getIncompatibleStatBadgeColor = (incompatible: number | undefined): string => + incompatible != null && incompatible > 0 ? 'danger' : 'hollow'; export const getErrorSummary = ({ error, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx index 417f1419a7ca5..329ff56cd8326 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx @@ -8,15 +8,15 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../mock/test_providers/test_providers'; +import { TestExternalProviders } from '../mock/test_providers/test_providers'; import { IlmPhasesEmptyPrompt } from '.'; describe('IlmPhasesEmptyPrompt', () => { beforeEach(() => { render( - + - + ); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx index 720f2fc61da6c..53d576ccf6f70 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx @@ -9,76 +9,36 @@ import { DARK_THEME } from '@elastic/charts'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from './mock/test_providers/test_providers'; +import { TestExternalProviders } from './mock/test_providers/test_providers'; import { DataQualityPanel } from '.'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; const { toasts } = notificationServiceMock.createSetupContract(); describe('DataQualityPanel', () => { - describe('when ILM phases are provided', () => { - const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; - - beforeEach(() => { - render( - - - - ); - }); - - test('it does NOT render the ILM phases empty prompt', () => { - expect(screen.queryByTestId('ilmPhasesEmptyPrompt')).not.toBeInTheDocument(); - }); - - test('it renders the body', () => { - expect(screen.getByTestId('body')).toBeInTheDocument(); - }); + beforeEach(() => { + render( + + + + ); }); - describe('when ILM phases are NOT provided', () => { - test('it renders the ILM phases empty prompt', () => { - const ilmPhases: string[] = []; - - render( - - - - ); - - expect(screen.getByTestId('ilmPhasesEmptyPrompt')).toBeInTheDocument(); - }); + test('it renders the body', () => { + expect(screen.getByTestId('body')).toBeInTheDocument(); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx index 6db2d8991db82..1fcedc7f76a82 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx @@ -7,23 +7,20 @@ import type { HttpHandler } from '@kbn/core-http-browser'; import numeral from '@elastic/numeral'; -import type { - FlameElementEvent, - HeatmapElementEvent, - MetricElementEvent, - PartialTheme, - PartitionElementEvent, - Theme, - WordCloudElementEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import React, { useCallback, useMemo } from 'react'; +import type { PartialTheme, Theme } from '@elastic/charts'; +import React, { useCallback, useMemo, useState } from 'react'; import type { IToasts } from '@kbn/core-notifications-browser'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; import { Body } from './data_quality_panel/body'; import { DataQualityProvider } from './data_quality_panel/data_quality_context'; import { EMPTY_STAT } from './helpers'; import { ReportDataQualityCheckAllCompleted, ReportDataQualityIndexChecked } from './types'; +import { ResultsRollupContext } from './contexts/results_rollup_context'; +import { IndicesCheckContext } from './contexts/indices_check_context'; +import { useIndicesCheck } from './use_indices_check'; +import { useResultsRollup } from './use_results_rollup'; +import { ilmPhaseOptionsStatic } from './constants'; interface Props { toasts: IToasts; @@ -32,21 +29,7 @@ interface Props { defaultNumberFormat: string; defaultBytesFormat: string; endDate?: string | null; - getGroupByFieldsOnClick: ( - elements: Array< - | FlameElementEvent - | HeatmapElementEvent - | MetricElementEvent - | PartitionElementEvent - | WordCloudElementEvent - | XYChartElementEvent - > - ) => { - groupByField0: string; - groupByField1: string; - }; httpFetch: HttpHandler; - ilmPhases: string[]; isAssistantEnabled: boolean; isILMAvailable: boolean; lastChecked: string; @@ -65,6 +48,10 @@ interface Props { theme?: PartialTheme; } +const defaultSelectedIlmPhaseOptions: EuiComboBoxOptionOption[] = ilmPhaseOptionsStatic.filter( + (option) => !option.disabled +); + /** Renders the `Data Quality` dashboard content */ const DataQualityPanelComponent: React.FC = ({ toasts, @@ -73,9 +60,7 @@ const DataQualityPanelComponent: React.FC = ({ defaultBytesFormat, defaultNumberFormat, endDate, - getGroupByFieldsOnClick, httpFetch, - ilmPhases, isAssistantEnabled, isILMAvailable, lastChecked, @@ -87,6 +72,9 @@ const DataQualityPanelComponent: React.FC = ({ startDate, theme, }) => { + const [selectedIlmPhaseOptions, setSelectedIlmPhaseOptions] = useState( + defaultSelectedIlmPhaseOptions + ); const formatBytes = useCallback( (value: number | undefined): string => value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT, @@ -110,6 +98,23 @@ const DataQualityPanelComponent: React.FC = ({ }, [toasts] ); + const ilmPhases: string[] = useMemo( + () => selectedIlmPhaseOptions.map(({ label }) => label), + [selectedIlmPhaseOptions] + ); + + const resultsRollupHookReturnValue = useResultsRollup({ + ilmPhases, + patterns, + httpFetch, + toasts, + isILMAvailable, + telemetryEvents, + }); + + const indicesCheckHookReturnValue = useIndicesCheck({ + onCheckCompleted: resultsRollupHookReturnValue.onCheckCompleted, + }); return ( = ({ telemetryEvents={telemetryEvents} isILMAvailable={isILMAvailable} toasts={toasts} + addSuccessToast={addSuccessToast} + canUserCreateAndReadCases={canUserCreateAndReadCases} + endDate={endDate} + formatBytes={formatBytes} + formatNumber={formatNumber} + isAssistantEnabled={isAssistantEnabled} + lastChecked={lastChecked} + openCreateCaseFlyout={openCreateCaseFlyout} + patterns={patterns} + setLastChecked={setLastChecked} + startDate={startDate} + theme={theme} + baseTheme={baseTheme} + ilmPhases={ilmPhases} + selectedIlmPhaseOptions={selectedIlmPhaseOptions} + setSelectedIlmPhaseOptions={setSelectedIlmPhaseOptions} > - + + + + + ); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx index b4579dd4bd50c..713b01e8ef71e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx @@ -13,29 +13,33 @@ import { I18nProvider } from '@kbn/i18n-react'; import { euiDarkVars } from '@kbn/ui-theme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; - import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { DataQualityProvider } from '../../data_quality_panel/data_quality_context'; +import { Theme } from '@elastic/charts'; + +import { + DataQualityProvider, + DataQualityProviderProps, +} from '../../data_quality_panel/data_quality_context'; +import { ResultsRollupContext } from '../../contexts/results_rollup_context'; +import { IndicesCheckContext } from '../../contexts/indices_check_context'; +import { UseIndicesCheckReturnValue } from '../../use_indices_check/types'; +import { UseResultsRollupReturnValue } from '../../use_results_rollup/types'; +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'; -interface Props { +interface TestExternalProvidersProps { children: React.ReactNode; - isILMAvailable?: boolean; } window.scrollTo = jest.fn(); /** A utility for wrapping children in the providers required to run tests */ -export const TestProvidersComponent: React.FC = ({ children, isILMAvailable = true }) => { - const http = httpServiceMock.createSetupContract({ basePath: '/test' }); - const { toasts } = notificationServiceMock.createSetupContract(); +const TestExternalProvidersComponent: React.FC = ({ children }) => { const actionTypeRegistry = actionTypeRegistryMock.create(); const mockGetComments = jest.fn(() => []); const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); const mockNavigateToApp = jest.fn(); - const mockTelemetryEvents = { - reportDataQualityIndexChecked: jest.fn(), - reportDataQualityCheckAllCompleted: jest.fn(), - }; const mockAssistantAvailability: AssistantAvailability = { hasAssistantPrivilege: false, hasConnectorsAllPrivilege: true, @@ -75,14 +79,7 @@ export const TestProvidersComponent: React.FC = ({ children, isILMAvailab navigateToApp={mockNavigateToApp} currentAppId={'securitySolutionUI'} > - - {children} - + {children} @@ -90,6 +87,90 @@ export const TestProvidersComponent: React.FC = ({ children, isILMAvailab ); }; -TestProvidersComponent.displayName = 'TestProvidersComponent'; +TestExternalProvidersComponent.displayName = 'TestExternalProvidersComponent'; + +export const TestExternalProviders = React.memo(TestExternalProvidersComponent); + +export interface TestDataQualityProvidersProps { + children: React.ReactNode; + dataQualityContextProps?: Partial; + indicesCheckContextProps?: Partial; + resultsRollupContextProps?: Partial; +} + +const TestDataQualityProvidersComponent: React.FC = ({ + children, + dataQualityContextProps, + resultsRollupContextProps, + indicesCheckContextProps, +}) => { + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const { toasts } = notificationServiceMock.createSetupContract(); + const mockTelemetryEvents = { + reportDataQualityIndexChecked: jest.fn(), + reportDataQualityCheckAllCompleted: jest.fn(), + }; + + const { + isILMAvailable, + addSuccessToast, + canUserCreateAndReadCases, + endDate, + formatBytes, + formatNumber, + isAssistantEnabled, + lastChecked, + openCreateCaseFlyout, + patterns, + setLastChecked, + startDate, + theme, + baseTheme, + ilmPhases, + selectedIlmPhaseOptions, + setSelectedIlmPhaseOptions, + } = getMergedDataQualityContextProps(dataQualityContextProps); + + const mergedResultsRollupContextProps = + getMergeResultsRollupContextProps(resultsRollupContextProps); + + return ( + + + + {children} + + + + ); +}; + +TestDataQualityProvidersComponent.displayName = 'TestDataQualityProvidersComponent'; -export const TestProviders = React.memo(TestProvidersComponent); +export const TestDataQualityProviders = React.memo(TestDataQualityProvidersComponent); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_data_quality_context_props.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_data_quality_context_props.ts new file mode 100644 index 0000000000000..d4cac9198e9c8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_data_quality_context_props.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import numeral from '@elastic/numeral'; + +import { DataQualityProviderProps } from '../../../data_quality_panel/data_quality_context'; +import { EMPTY_STAT } from '../../../helpers'; + +export const getMergedDataQualityContextProps = ( + dataQualityContextProps?: Partial +) => { + const { + isILMAvailable, + addSuccessToast, + canUserCreateAndReadCases, + endDate, + formatBytes, + formatNumber, + isAssistantEnabled, + lastChecked, + openCreateCaseFlyout, + patterns, + setLastChecked, + startDate, + theme, + baseTheme, + ilmPhases, + selectedIlmPhaseOptions, + setSelectedIlmPhaseOptions, + } = { + isILMAvailable: true, + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: jest.fn(() => true), + endDate: null, + formatBytes: (value: number | undefined) => + value != null ? numeral(value).format('0,0.[0]b') : EMPTY_STAT, + formatNumber: (value: number | undefined) => + value != null ? numeral(value).format('0,0.[000]') : EMPTY_STAT, + isAssistantEnabled: true, + lastChecked: '2023-03-28T22:27:28.159Z', + openCreateCaseFlyout: jest.fn(), + patterns: ['auditbeat-*'], + setLastChecked: jest.fn(), + startDate: null, + theme: { + background: { + color: '#000', + }, + }, + baseTheme: { + background: { + color: '#000', + }, + }, + ilmPhases: ['hot', 'warm', 'unmanaged'], + selectedIlmPhaseOptions: [ + { + label: 'hot', + value: 'hot', + }, + { + label: 'warm', + value: 'warm', + }, + { + label: 'unmanaged', + value: 'unmanaged', + }, + ], + setSelectedIlmPhaseOptions: jest.fn(), + ...dataQualityContextProps, + }; + + return { + isILMAvailable, + addSuccessToast, + canUserCreateAndReadCases, + endDate, + formatBytes, + formatNumber, + isAssistantEnabled, + lastChecked, + openCreateCaseFlyout, + patterns, + setLastChecked, + startDate, + theme, + baseTheme, + ilmPhases, + selectedIlmPhaseOptions, + setSelectedIlmPhaseOptions, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_indices_check_context_props.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_indices_check_context_props.ts new file mode 100644 index 0000000000000..28129d7e8155f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_indices_check_context_props.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getCheckState } from '../../../stub/get_check_state'; +import { + UseIndicesCheckCheckState, + UseIndicesCheckReturnValue, +} from '../../../use_indices_check/types'; + +export const getMergedIndicesCheckContextProps = ( + patternIndexNames: Record, + indicesCheckContextProps?: Partial +): UseIndicesCheckReturnValue => { + const checkState = Object.keys(patternIndexNames).reduce( + (acc, key) => { + for (const indexName of patternIndexNames[key]) { + acc[indexName] = { + ...getCheckState(indexName)[indexName], + }; + } + + return acc; + }, + {} + ); + + return { + checkIndex: jest.fn(), + checkState, + ...indicesCheckContextProps, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_results_rollup_context_props.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_results_rollup_context_props.ts new file mode 100644 index 0000000000000..bb8e3e6967b4d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/utils/get_merged_results_rollup_context_props.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UseResultsRollupReturnValue } from '../../../use_results_rollup/types'; +import { auditbeatWithAllResults } from '../../pattern_rollup/mock_auditbeat_pattern_rollup'; + +export const getMergeResultsRollupContextProps = ( + resultsRollupContextProps?: Partial +) => { + const { + onCheckCompleted, + patternIndexNames, + patternRollups, + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + totalSameFamily, + totalSizeInBytes, + updatePatternIndexNames, + updatePatternRollup, + } = { + onCheckCompleted: jest.fn(), + patternIndexNames: { + 'auditbeat-*': [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-index-1', + 'auditbeat-custom-empty-index-1', + ], + }, + patternRollups: { + 'auditbeat-*': auditbeatWithAllResults, + }, + totalDocsCount: 19127, + totalIncompatible: 4, + totalIndices: 3, + totalIndicesChecked: 3, + totalSameFamily: 0, + totalSizeInBytes: 18820446, + updatePatternIndexNames: jest.fn(), + updatePatternRollup: jest.fn(), + ...resultsRollupContextProps, + }; + + return { + onCheckCompleted, + patternIndexNames, + patternRollups, + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + totalSameFamily, + totalSizeInBytes, + updatePatternIndexNames, + updatePatternRollup, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/stub/get_check_state/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/stub/get_check_state/index.ts new file mode 100644 index 0000000000000..c1ba27ac91376 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/stub/get_check_state/index.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 { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; + +import { + getMappingsProperties, + getSortedPartitionedFieldMetadata, +} from '../../data_quality_panel/index_properties/helpers'; +import { mockMappingsResponse } from '../../mock/mappings_response/mock_mappings_response'; +import { UseIndicesCheckCheckState } from '../../use_indices_check/types'; +import { getUnallowedValues } from '../../use_unallowed_values/helpers'; +import { getUnallowedValueRequestItems } from '../../data_quality_panel/allowed_values/helpers'; +import { EcsFlatTyped } from '../../constants'; +import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_unallowed_values'; +import { UnallowedValueSearchResult } from '../../types'; + +export const getCheckState = ( + indexName: string, + indexCheckState?: Partial +) => { + const mappingsProperties = getMappingsProperties({ + indexName, + indexes: mockMappingsResponse as Record, + }); + const unallowedValues = getUnallowedValues({ + requestItems: getUnallowedValueRequestItems({ + ecsMetadata: EcsFlatTyped, + indexName, + }), + searchResults: mockUnallowedValuesResponse as unknown as UnallowedValueSearchResult[], + }); + const partitionedFieldMetadata = getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties, + unallowedValues, + }); + return { + [indexName]: { + isChecking: false, + isLoadingMappings: false, + isLoadingUnallowedValues: false, + indexes: mockMappingsResponse as Record, + partitionedFieldMetadata, + searchResults: mockUnallowedValuesResponse as unknown as UnallowedValueSearchResult[], + unallowedValues, + mappingsProperties, + genericError: null, + mappingsError: null, + unallowedValuesError: null, + isCheckComplete: true, + ...indexCheckState, + }, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/styles.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/styles.tsx index 6fbf130d01b8f..d54ea9d6316e2 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/styles.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/styles.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCode, EuiText } from '@elastic/eui'; +import { EuiCode } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import styled from 'styled-components'; @@ -21,10 +21,3 @@ export const CodeSuccess = styled(EuiCode)` export const CodeWarning = styled(EuiCode)` color: ${euiThemeVars.euiColorWarning}; `; - -export const FixedWidthLegendText = styled(EuiText)<{ - $width: number | undefined; -}>` - text-align: left; - ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} -`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/translations.ts index b72a9fee96c57..6c78513274de8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/translations.ts @@ -248,7 +248,7 @@ export const SELECT_ONE_OR_MORE_ILM_PHASES: string = i18n.translate( export const INDEX_SIZE_TOOLTIP = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip', { - defaultMessage: 'The size of the primary index (does not include replicas)', + defaultMessage: 'Size of index (exluding replicas)', } ); @@ -302,3 +302,42 @@ export const GET_RESULTS_ERROR_TITLE = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.getResultErrorTitle', { defaultMessage: 'Error reading saved data quality check results' } ); + +export const COLD = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCold', + { + defaultMessage: 'cold', + } +); + +export const FROZEN = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseFrozen', + { + defaultMessage: 'frozen', + } +); + +export const HOT = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot', { + defaultMessage: 'hot', +}); + +export const WARM = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm', + { + defaultMessage: 'warm', + } +); + +export const UNMANAGED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged', + { + defaultMessage: 'unmanaged', + } +); + +export const DATA_QUALITY_DASHBOARD_CONVERSATION_ID = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.dataQualityDashboardConversationId', + { + defaultMessage: 'Data Quality dashboard', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts index ed20efc3fd959..dd5dd6dd0a159 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts @@ -200,15 +200,15 @@ export interface IndexToCheck { export type OnCheckCompleted = (param: { batchId: string; + isCheckAll: boolean; + isLastCheck: boolean; checkAllStartTime: number; error: string | null; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; - isLastCheck: boolean; partitionedFieldMetadata: PartitionedFieldMetadata | null; pattern: string; - version: string; requestTime?: number; }) => void; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.test.tsx new file mode 100644 index 0000000000000..ffbeb191e5582 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useCurrentWindowWidth } from '.'; +import { fireEvent } from '@testing-library/react'; + +describe('useCurrentWidthWidth', () => { + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1024, + }); + }); + it('return current window width', () => { + const { result } = renderHook(() => useCurrentWindowWidth()); + expect(result.error).toBeUndefined(); + expect(result.current).toBe(window.innerWidth); + }); + + it('return last-throttled value of window.innerWidth with interval of 250ms', async () => { + jest.useFakeTimers(); + const { result } = renderHook(() => useCurrentWindowWidth()); + + // first resize within throttle interval + fireEvent.resize(window, { target: { innerWidth: 500 } }); + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(result.current).toBe(1024); + + // second resize within throttle interval + fireEvent.resize(window, { target: { innerWidth: 400 } }); + act(() => { + jest.advanceTimersByTime(49); + }); + + expect(result.current).toBe(1024); + + // third and final resize after throttle interval + fireEvent.resize(window, { target: { innerWidth: 600 } }); + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(result.current).toBe(600); + + // release all timers to confirm the final value + act(() => { + jest.runAllTimers(); + }); + + expect(result.current).toBe(600); + + jest.useRealTimers(); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.tsx new file mode 100644 index 0000000000000..0ef0febcded4d --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_current_window_width/index.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import throttle from 'lodash/throttle'; +import { useEffect, useState } from 'react'; + +export const useCurrentWindowWidth = () => { + const [currentWidth, setCurrentWidth] = useState(() => window.innerWidth); + + useEffect(() => { + const handleWindowResize = throttle( + () => { + setCurrentWidth(window.innerWidth); + }, + 250, + { leading: false } + ); + + window.addEventListener('resize', handleWindowResize); + + return () => window.removeEventListener('resize', handleWindowResize); + }, []); + + return currentWidth; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx index f715936501736..b8c6092fea0d0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx @@ -13,6 +13,7 @@ import { mockIlmExplain } from '../mock/ilm_explain/mock_ilm_explain'; import { ERROR_LOADING_ILM_EXPLAIN } from '../translations'; import { useIlmExplain, UseIlmExplain } from '.'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; +import { Theme } from '@elastic/charts'; const mockHttpFetch = jest.fn(); const mockReportDataQualityIndexChecked = jest.fn(); @@ -31,6 +32,45 @@ const ContextWrapper: React.FC<{ children: React.ReactNode; isILMAvailable: bool telemetryEvents={mockTelemetryEvents} isILMAvailable={isILMAvailable} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} @@ -80,6 +120,45 @@ describe('useIlmExplain', () => { telemetryEvents={mockTelemetryEvents} isILMAvailable={false} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.tsx index 4e95549338874..8477d710fea76 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.tsx @@ -11,6 +11,7 @@ import { useEffect, useState } from 'react'; import { useDataQualityContext } from '../data_quality_panel/data_quality_context'; import { INTERNAL_API_VERSION } from '../helpers'; import * as i18n from '../translations'; +import { useIsMounted } from '../use_is_mounted'; const ILM_EXPLAIN_ENDPOINT = '/internal/ecs_data_quality_dashboard/ilm_explain'; @@ -22,6 +23,7 @@ export interface UseIlmExplain { export const useIlmExplain = (pattern: string): UseIlmExplain => { const { httpFetch, isILMAvailable } = useDataQualityContext(); + const { isMountedRef } = useIsMounted(); const [ilmExplain, setIlmExplain] = useState { ); if (!abortController.signal.aborted) { - setIlmExplain(response); + if (isMountedRef.current) { + setIlmExplain(response); + } } } catch (e) { if (!abortController.signal.aborted) { - setError(i18n.ERROR_LOADING_ILM_EXPLAIN(e.message)); + if (isMountedRef.current) { + setError(i18n.ERROR_LOADING_ILM_EXPLAIN(e.message)); + } } } finally { - setLoading(false); + if (isMountedRef.current) { + setLoading(false); + } } } @@ -65,7 +73,7 @@ export const useIlmExplain = (pattern: string): UseIlmExplain => { return () => { abortController.abort(); }; - }, [httpFetch, isILMAvailable, pattern, setError]); + }, [httpFetch, isILMAvailable, isMountedRef, pattern, setError]); return { ilmExplain, error, loading }; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.test.tsx new file mode 100644 index 0000000000000..b129102e5516e --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.test.tsx @@ -0,0 +1,386 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { useIndicesCheck } from '.'; + +import * as utilsCheckIndex from '../utils/check_index'; +import { mockUnallowedValuesResponse } from '../mock/unallowed_values/mock_unallowed_values'; +import { mockMappingsResponse } from '../mock/mappings_response/mock_mappings_response'; +import { HttpHandler } from '@kbn/core-http-browser'; +import { MappingsError } from '../use_mappings/helpers'; +import { UnallowedValuesError } from '../use_unallowed_values/helpers'; +import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; +import { UnallowedValueSearchResult } from '../types'; +import { getInitialCheckStateValue } from './reducer'; + +const getSpies = () => { + return { + checkIndexSpy: jest.spyOn(utilsCheckIndex, 'checkIndex').mockImplementation(jest.fn()), + }; +}; + +describe('useIndicesCheck', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return checkIndex and checkState', () => { + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + expect(result.current).toEqual({ + checkIndex: expect.any(Function), + checkState: expect.any(Object), + }); + }); + + describe('checkIndex', () => { + it('should call checkIndex with the correct arguments', () => { + const { checkIndexSpy } = getSpies(); + + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + const props = { + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'indexName', + pattern: 'pattern', + httpFetch: jest.fn(), + }; + + act(() => { + result.current.checkIndex(props); + }); + + expect(checkIndexSpy).toHaveBeenCalledWith({ + ...props, + onCheckCompleted: expect.any(Function), + onLoadMappingsStart: expect.any(Function), + onLoadMappingsSuccess: expect.any(Function), + onLoadUnallowedValuesStart: expect.any(Function), + onLoadUnallowedValuesSuccess: expect.any(Function), + onStart: expect.any(Function), + onSuccess: expect.any(Function), + onError: expect.any(Function), + }); + }); + }); + + describe('checkState', () => { + it('should be empty by default', () => { + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + expect(result.current.checkState).toEqual({}); + }); + + describe('when checkIndex completes', () => { + it('should set correct data', async () => { + const { result, waitFor } = renderHook(() => + useIndicesCheck({ onCheckCompleted: jest.fn() }) + ); + + const httpFetchMock = jest.fn((route) => { + if (route.startsWith('/internal/ecs_data_quality_dashboard/mappings')) { + return Promise.resolve(mockMappingsResponse); + } + + if (route.startsWith('/internal/ecs_data_quality_dashboard/unallowed_field_values')) { + return Promise.resolve(mockUnallowedValuesResponse); + } + }); + + act(() => { + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: httpFetchMock as unknown as HttpHandler, + }); + }); + + await waitFor(() => + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + indexes: expect.any(Object), + partitionedFieldMetadata: expect.any(Object), + searchResults: expect.any(Object), + unallowedValues: expect.any(Object), + mappingsProperties: expect.any(Object), + isCheckComplete: true, + }) + ); + }); + }); + + describe('errors', () => { + describe('when mappings request errors', () => { + it('should set mappingsError', async () => { + const { result, waitFor } = renderHook(() => + useIndicesCheck({ onCheckCompleted: jest.fn() }) + ); + + const httpFetchMock = jest.fn((route) => { + if (route.startsWith('/internal/ecs_data_quality_dashboard/mappings')) { + return Promise.reject(new Error('mappings error')); + } + + if (route.startsWith('/internal/ecs_data_quality_dashboard/unallowed_field_values')) { + return Promise.reject(new Error('unallowed values error')); + } + }); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: httpFetchMock as unknown as HttpHandler, + }) + ); + + await waitFor(() => + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + mappingsError: expect.any(MappingsError), + }) + ); + }); + }); + + describe('when unallowed values request errors', () => { + it('should set unallowedValuesError', async () => { + const { result, waitFor } = renderHook(() => + useIndicesCheck({ onCheckCompleted: jest.fn() }) + ); + + const httpFetchMock = jest.fn((route) => { + if (route.startsWith('/internal/ecs_data_quality_dashboard/mappings')) { + return Promise.resolve(mockMappingsResponse); + } + + if (route.startsWith('/internal/ecs_data_quality_dashboard/unallowed_field_values')) { + return Promise.reject(new Error('unallowed values error')); + } + }); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: httpFetchMock as unknown as HttpHandler, + }) + ); + + await waitFor(() => + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + unallowedValuesError: expect.any(UnallowedValuesError), + }) + ); + }); + }); + + describe('when anything else errors', () => { + it('should set genericError', () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onError }) => { + onError?.(new Error('generic error')); + }); + + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + genericError: expect.any(Error), + }); + }); + }); + }); + + describe('lifecycle states', () => { + describe('when check is started', () => { + it('it should set isChecking to true', async () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onStart }) => { + onStart?.(); + }); + + const { result, waitFor } = renderHook(() => + useIndicesCheck({ onCheckCompleted: jest.fn() }) + ); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + await waitFor(() => + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + isChecking: true, + }) + ); + }); + }); + + describe('when mappings are loading', () => { + it('it should set isLoadingMappings to true', () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onStart, onLoadMappingsStart }) => { + onStart?.(); + onLoadMappingsStart?.(); + }); + + const { result, waitFor } = renderHook(() => + useIndicesCheck({ onCheckCompleted: jest.fn() }) + ); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + waitFor(() => + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + isChecking: true, + isLoadingMappings: true, + }) + ); + }); + }); + + describe('when unallowed values are loading', () => { + it('it should set isLoadingUnallowedValues to true', () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onStart, onLoadUnallowedValuesStart }) => { + onStart?.(); + onLoadUnallowedValuesStart?.(); + }); + + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + isChecking: true, + isLoadingUnallowedValues: true, + }); + }); + }); + + describe('when mappings are loaded', () => { + it('should set indexes', () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onStart, onLoadMappingsSuccess }) => { + onStart?.(); + onLoadMappingsSuccess?.( + mockMappingsResponse as Record + ); + }); + + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + indexes: mockMappingsResponse, + }); + }); + }); + + describe('when unallowed values are loaded', () => { + it('should set searchResults', () => { + const { checkIndexSpy } = getSpies(); + + checkIndexSpy.mockImplementation(async ({ onStart, onLoadUnallowedValuesSuccess }) => { + onStart?.(); + onLoadUnallowedValuesSuccess?.( + mockUnallowedValuesResponse as unknown as UnallowedValueSearchResult[] + ); + }); + + const { result } = renderHook(() => useIndicesCheck({ onCheckCompleted: jest.fn() })); + + act(() => + result.current.checkIndex({ + abortController: new AbortController(), + formatBytes: jest.fn(), + formatNumber: jest.fn(), + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + httpFetch: jest.fn(), + }) + ); + + expect(result.current.checkState['auditbeat-custom-index-1']).toEqual({ + ...getInitialCheckStateValue(), + searchResults: mockUnallowedValuesResponse, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.tsx new file mode 100644 index 0000000000000..acbad56a613c5 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/index.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useReducer, useCallback } from 'react'; +import { OnCheckCompleted } from '../types'; +import { MappingsError } from '../use_mappings/helpers'; +import { UnallowedValuesError } from '../use_unallowed_values/helpers'; +import { checkIndex as _checkIndex, CheckIndexProps } from '../utils/check_index'; +import { initialState, reducer } from './reducer'; +import { UseIndicesCheckReturnValue } from './types'; +import { useIsMounted } from '../use_is_mounted'; + +export const useIndicesCheck = ({ + onCheckCompleted, +}: { + onCheckCompleted: OnCheckCompleted; +}): UseIndicesCheckReturnValue => { + const { isMountedRef } = useIsMounted(); + const [state, dispatch] = useReducer(reducer, initialState); + + const checkIndex = useCallback( + ({ + abortController, + formatBytes, + formatNumber, + httpFetch, + indexName, + pattern, + batchId, + checkAllStartTime, + isCheckAll, + isLastCheck, + }: Omit) => { + _checkIndex({ + batchId, + abortController, + formatBytes, + formatNumber, + httpFetch, + indexName, + onCheckCompleted, + pattern, + checkAllStartTime, + isCheckAll, + isLastCheck, + onStart: () => { + if (!isMountedRef.current) return; + dispatch({ type: 'START', data: { indexName } }); + }, + onLoadMappingsStart: () => { + if (!isMountedRef.current) return; + dispatch({ type: 'LOAD_MAPPINGS_START', data: { indexName } }); + }, + onLoadUnallowedValuesStart: () => { + if (!isMountedRef.current) return; + dispatch({ type: 'LOAD_UNALLOWED_VALUES_START', data: { indexName } }); + }, + onSuccess: ({ partitionedFieldMetadata, mappingsProperties, unallowedValues }) => { + if (!isMountedRef.current) return; + dispatch({ + type: 'SUCCESS', + data: { + indexName, + partitionedFieldMetadata, + mappingsProperties, + unallowedValues, + }, + }); + }, + onLoadMappingsSuccess: (indexes) => { + if (!isMountedRef.current) return; + dispatch({ type: 'LOAD_MAPPINGS_SUCCESS', data: { indexName, indexes } }); + }, + onLoadUnallowedValuesSuccess: (searchResults) => { + if (!isMountedRef.current) return; + dispatch({ type: 'LOAD_UNALLOWED_VALUES_SUCCESS', data: { indexName, searchResults } }); + }, + onError: (error) => { + if (!isMountedRef.current) return; + if (error instanceof MappingsError) { + dispatch({ type: 'LOAD_MAPPINGS_ERROR', data: { indexName, error } }); + } else if (error instanceof UnallowedValuesError) { + dispatch({ type: 'LOAD_UNALLOWED_VALUES_ERROR', data: { indexName, error } }); + } else { + if (error instanceof Error) { + dispatch({ type: 'GENERIC_ERROR', data: { indexName, error } }); + } else { + dispatch({ type: 'GENERIC_ERROR', data: { indexName, error: 'Unknown error' } }); + } + } + }, + }); + }, + [isMountedRef, onCheckCompleted] + ); + + return { + ...state, + checkIndex, + }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/reducer.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/reducer.ts new file mode 100644 index 0000000000000..ca596643605ce --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/reducer.ts @@ -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 { + IndicesGetMappingIndexMappingRecord, + MappingProperty, +} from '@elastic/elasticsearch/lib/api/types'; + +import { + PartitionedFieldMetadata, + UnallowedValueCount, + UnallowedValueSearchResult, +} from '../types'; +import { MappingsError } from '../use_mappings/helpers'; +import { UnallowedValuesError } from '../use_unallowed_values/helpers'; +import { UseIndicesCheckState } from './types'; + +type Action = { data: { indexName: string } } & ( + | { + type: 'START' | 'LOAD_MAPPINGS_START' | 'LOAD_UNALLOWED_VALUES_START'; + } + | { + type: 'SUCCESS'; + data: { + partitionedFieldMetadata: PartitionedFieldMetadata; + mappingsProperties: Record | null; + unallowedValues: Record; + }; + } + | { + type: 'LOAD_MAPPINGS_SUCCESS'; + data: { indexes: Record }; + } + | { + type: 'LOAD_UNALLOWED_VALUES_SUCCESS'; + data: { searchResults: UnallowedValueSearchResult[] }; + } + | { type: 'GENERIC_ERROR'; data: { error: string | Error } } + | { type: 'LOAD_MAPPINGS_ERROR'; data: { error: MappingsError } } + | { type: 'LOAD_UNALLOWED_VALUES_ERROR'; data: { error: UnallowedValuesError } } +); + +// intentionally returning a new object every time +// instead of caching the initial state +// to avoid potential mutations when spreaded +// across actions +export const getInitialCheckStateValue = () => ({ + isChecking: false, + isLoadingMappings: false, + isLoadingUnallowedValues: false, + indexes: null, + partitionedFieldMetadata: null, + searchResults: null, + unallowedValues: null, + mappingsProperties: null, + genericError: null, + mappingsError: null, + unallowedValuesError: null, + isCheckComplete: false, +}); + +export const initialState: UseIndicesCheckState = { + checkState: {}, +}; + +export const reducer = (state: UseIndicesCheckState, action: Action): UseIndicesCheckState => { + switch (action.type) { + case 'START': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...getInitialCheckStateValue(), + isChecking: true, + }, + }, + }; + case 'LOAD_MAPPINGS_START': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...state.checkState[action.data.indexName], + isChecking: true, + isLoadingMappings: true, + }, + }, + }; + case 'LOAD_UNALLOWED_VALUES_START': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...state.checkState[action.data.indexName], + isChecking: true, + isLoadingUnallowedValues: true, + }, + }, + }; + case 'SUCCESS': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...state.checkState[action.data.indexName], + isChecking: false, + isLoadingMappings: false, + isLoadingUnallowedValues: false, + genericError: null, + mappingsError: null, + unallowedValuesError: null, + partitionedFieldMetadata: action.data.partitionedFieldMetadata, + unallowedValues: action.data.unallowedValues, + mappingsProperties: action.data.mappingsProperties, + isCheckComplete: true, + }, + }, + }; + case 'LOAD_MAPPINGS_SUCCESS': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...state.checkState[action.data.indexName], + isChecking: false, + isLoadingMappings: false, + isLoadingUnallowedValues: false, + genericError: null, + mappingsError: null, + indexes: action.data.indexes, + }, + }, + }; + case 'LOAD_UNALLOWED_VALUES_SUCCESS': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...state.checkState[action.data.indexName], + isChecking: false, + isLoadingUnallowedValues: false, + genericError: null, + unallowedValuesError: null, + searchResults: action.data.searchResults, + }, + }, + }; + case 'GENERIC_ERROR': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...getInitialCheckStateValue(), + genericError: action.data.error, + }, + }, + }; + case 'LOAD_MAPPINGS_ERROR': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...getInitialCheckStateValue(), + mappingsError: action.data.error, + }, + }, + }; + case 'LOAD_UNALLOWED_VALUES_ERROR': + return { + ...state, + checkState: { + ...state.checkState, + [action.data.indexName]: { + ...getInitialCheckStateValue(), + unallowedValuesError: action.data.error, + }, + }, + }; + default: + throw new Error('Invalid action type'); + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/types.ts new file mode 100644 index 0000000000000..c7f0145f68f15 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_indices_check/types.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + IndicesGetMappingIndexMappingRecord, + MappingProperty, +} from '@elastic/elasticsearch/lib/api/types'; +import { + PartitionedFieldMetadata, + UnallowedValueCount, + UnallowedValueSearchResult, +} from '../types'; +import { MappingsError } from '../use_mappings/helpers'; +import { UnallowedValuesError } from '../use_unallowed_values/helpers'; +import { CheckIndexProps } from '../utils/check_index'; + +export interface UseIndicesCheckCheckState { + [indexName: string]: { + isChecking: boolean; + isLoadingMappings: boolean; + isLoadingUnallowedValues: boolean; + indexes: Record | null; + partitionedFieldMetadata: PartitionedFieldMetadata | null; + searchResults: UnallowedValueSearchResult[] | null; + unallowedValues: Record | null; + mappingsProperties: Record | null; + genericError: string | Error | null; + mappingsError: MappingsError | null; + unallowedValuesError: UnallowedValuesError | null; + isCheckComplete: boolean; + }; +} + +export interface UseIndicesCheckState { + checkState: UseIndicesCheckCheckState; +} + +export interface UseIndicesCheckReturnValue extends UseIndicesCheckState { + checkIndex: (props: Omit) => void; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.test.tsx new file mode 100644 index 0000000000000..d29aaf8ace201 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { useIsMounted } from '.'; + +describe('useIsMounted', () => { + it('should return a ref that is true when mounted and false when unmounted', () => { + const { result, unmount } = renderHook(() => useIsMounted()); + + expect(result.current.isMountedRef.current).toBe(true); + + act(() => { + unmount(); + }); + + expect(result.current.isMountedRef.current).toBe(false); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.tsx new file mode 100644 index 0000000000000..6ba5ebacf8ea0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_is_mounted/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MutableRefObject, useEffect, useRef } from 'react'; + +/** + * Hook that returns a ref that is true when mounted and false when unmounted. + * + * Main use case is to avoid setting state on an unmounted component. + */ +export const useIsMounted = (): { isMountedRef: MutableRefObject } => { + const isMountedRef = useRef(true); + + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + return { isMountedRef }; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/helpers.ts index 809f543c0c0ae..6bee012883c05 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/helpers.ts @@ -34,6 +34,16 @@ export async function fetchMappings({ } ); } catch (e) { - throw new Error(i18n.ERROR_LOADING_MAPPINGS({ details: e.message, patternOrIndexName })); + throw new MappingsError(patternOrIndexName, e.message); + } +} + +export class MappingsError extends Error { + constructor(patternOrIndexName: string, details: string) { + const message = i18n.ERROR_LOADING_MAPPINGS({ details, patternOrIndexName }); + super(message); + + this.name = 'MappingsError'; + this.message = message; } } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx index 8ccac0df997e7..86b3ef1688e6a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx @@ -13,6 +13,7 @@ import { mockMappingsResponse } from '../mock/mappings_response/mock_mappings_re import { ERROR_LOADING_MAPPINGS } from '../translations'; import { useMappings, UseMappings } from '.'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; +import { Theme } from '@elastic/charts'; const mockHttpFetch = jest.fn(); const mockReportDataQualityIndexChecked = jest.fn(); @@ -29,6 +30,45 @@ const ContextWrapper: FC> = ({ children }) => ( telemetryEvents={mockTelemetryEvents} isILMAvailable={true} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.tsx index f0f676ff24cac..f20809a248086 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'; import { useDataQualityContext } from '../data_quality_panel/data_quality_context'; import { fetchMappings } from './helpers'; +import { useIsMounted } from '../use_is_mounted'; export interface UseMappings { indexes: Record | null; @@ -18,6 +19,7 @@ export interface UseMappings { } export const useMappings = (patternOrIndexName: string): UseMappings => { + const { isMountedRef } = useIsMounted(); const [indexes, setIndexes] = useState { const response = await fetchMappings({ abortController, httpFetch, patternOrIndexName }); if (!abortController.signal.aborted) { - setIndexes(response); + if (isMountedRef.current) { + setIndexes(response); + } } } catch (e) { if (!abortController.signal.aborted) { - setError(e.message); + if (isMountedRef.current) { + setError(e.message); + } } } finally { if (!abortController.signal.aborted) { - setLoading(false); + if (isMountedRef.current) { + setLoading(false); + } } } } @@ -52,7 +60,7 @@ export const useMappings = (patternOrIndexName: string): UseMappings => { return () => { abortController.abort(); }; - }, [httpFetch, patternOrIndexName, setError]); + }, [httpFetch, isMountedRef, patternOrIndexName, setError]); return { indexes, error, loading }; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx index 7a53eedc3c562..f0c6f9d28e501 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx @@ -9,6 +9,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { EcsVersion } from '@elastic/ecs'; import { isEmpty } from 'lodash/fp'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { HttpHandler } from '@kbn/core-http-browser'; import { getTotalDocsCount, getTotalIncompatible, @@ -19,7 +21,12 @@ import { updateResultOnCheckCompleted, } from './helpers'; -import type { DataQualityCheckResult, OnCheckCompleted, PatternRollup } from '../types'; +import type { + DataQualityCheckResult, + OnCheckCompleted, + PatternRollup, + TelemetryEvents, +} from '../types'; import { getDocsCount, getIndexId, @@ -31,39 +38,24 @@ import { formatResultFromStorage, } from '../helpers'; import { getIlmPhase, getIndexIncompatible } from '../data_quality_panel/pattern/helpers'; -import { useDataQualityContext } from '../data_quality_panel/data_quality_context'; import { getIncompatibleMappingsFields, getIncompatibleValuesFields, getSameFamilyFields, } from '../data_quality_panel/tabs/incompatible_tab/helpers'; +import { UseResultsRollupReturnValue } from './types'; +import { useIsMounted } from '../use_is_mounted'; interface Props { ilmPhases: string[]; patterns: string[]; + toasts: IToasts; + httpFetch: HttpHandler; + telemetryEvents: TelemetryEvents; + isILMAvailable: boolean; } -interface UseResultsRollup { - onCheckCompleted: OnCheckCompleted; - patternIndexNames: Record; - patternRollups: Record; - totalDocsCount: number | undefined; - totalIncompatible: number | undefined; - totalIndices: number | undefined; - totalIndicesChecked: number | undefined; - totalSameFamily: number | undefined; - totalSizeInBytes: number | undefined; - updatePatternIndexNames: ({ - indexNames, - pattern, - }: { - indexNames: string[]; - pattern: string; - }) => void; - updatePatternRollup: (patternRollup: PatternRollup) => void; -} - -const useStoredPatternResults = (patterns: string[]) => { - const { httpFetch, toasts } = useDataQualityContext(); +const useStoredPatternResults = (patterns: string[], toasts: IToasts, httpFetch: HttpHandler) => { + const { isMountedRef } = useIsMounted(); const [storedPatternResults, setStoredPatternResults] = useState< Array<{ pattern: string; results: Record }> >([]); @@ -89,7 +81,9 @@ const useStoredPatternResults = (patterns: string[]) => { ); const patternResults = await Promise.all(requests); if (patternResults?.length && !ignore) { - setStoredPatternResults(patternResults); + if (isMountedRef.current) { + setStoredPatternResults(patternResults); + } } }; @@ -97,17 +91,23 @@ const useStoredPatternResults = (patterns: string[]) => { return () => { ignore = true; }; - }, [httpFetch, patterns, toasts]); + }, [httpFetch, isMountedRef, patterns, toasts]); return storedPatternResults; }; -export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRollup => { - const { httpFetch, toasts } = useDataQualityContext(); +export const useResultsRollup = ({ + httpFetch, + toasts, + ilmPhases, + patterns, + isILMAvailable, + telemetryEvents, +}: Props): UseResultsRollupReturnValue => { const [patternIndexNames, setPatternIndexNames] = useState>({}); const [patternRollups, setPatternRollups] = useState>({}); - const storedPatternsResults = useStoredPatternResults(patterns); + const storedPatternsResults = useStoredPatternResults(patterns, toasts, httpFetch); useEffect(() => { if (!isEmpty(storedPatternsResults)) { @@ -127,7 +127,6 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll } }, [storedPatternsResults]); - const { telemetryEvents, isILMAvailable } = useDataQualityContext(); const updatePatternRollup = useCallback((patternRollup: PatternRollup) => { setPatternRollups((current) => ({ ...current, @@ -167,6 +166,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll pattern, requestTime, isLastCheck, + isCheckAll, }) => { setPatternRollups((currentPatternRollups) => { const updatedRollups = updateResultOnCheckCompleted({ @@ -198,7 +198,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll ilmPhase: getIlmPhase(ilmExplain?.[indexName], isILMAvailable), indexId, indexName, - isCheckAll: true, + isCheckAll, numberOfDocuments: getDocsCount({ indexName, stats }), numberOfFields: partitionedFieldMetadata.all.length, numberOfIncompatibleFields: getIndexIncompatible({ diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/types.ts new file mode 100644 index 0000000000000..e8f0124cd4a3f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OnCheckCompleted, PatternRollup } from '../types'; + +export interface UseResultsRollupReturnValue { + onCheckCompleted: OnCheckCompleted; + patternIndexNames: Record; + patternRollups: Record; + totalDocsCount: number | undefined; + totalIncompatible: number | undefined; + totalIndices: number | undefined; + totalIndicesChecked: number | undefined; + totalSameFamily: number | undefined; + totalSizeInBytes: number | undefined; + updatePatternIndexNames: ({ + indexNames, + pattern, + }: { + indexNames: string[]; + pattern: string; + }) => void; + updatePatternRollup: (patternRollup: PatternRollup) => void; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx index d084f168ac450..c07bf9b8afd76 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx @@ -13,6 +13,7 @@ import { mockStatsAuditbeatIndex } from '../mock/stats/mock_stats_packetbeat_ind import { ERROR_LOADING_STATS } from '../translations'; import { useStats, UseStats } from '.'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; +import { Theme } from '@elastic/charts'; const mockHttpFetch = jest.fn(); const mockReportDataQualityIndexChecked = jest.fn(); @@ -29,6 +30,45 @@ const ContextWrapper: FC> = ({ children }) => ( telemetryEvents={mockTelemetryEvents} isILMAvailable={true} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} @@ -40,6 +80,45 @@ const ContextWrapperILMNotAvailable: FC> = ({ childre telemetryEvents={mockTelemetryEvents} isILMAvailable={false} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.tsx index 90af7ded0ba00..0de57ccd54568 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.tsx @@ -12,6 +12,7 @@ import { useDataQualityContext } from '../data_quality_panel/data_quality_contex import * as i18n from '../translations'; import { INTERNAL_API_VERSION } from '../helpers'; import { MeteringStatsIndex } from '../types'; +import { useIsMounted } from '../use_is_mounted'; const STATS_ENDPOINT = '/internal/ecs_data_quality_dashboard/stats'; @@ -30,6 +31,7 @@ export const useStats = ({ pattern: string; startDate?: string | null; }): UseStats => { + const { isMountedRef } = useIsMounted(); const { httpFetch, isILMAvailable } = useDataQualityContext(); const [stats, setStats] = useState | null>(null); const [error, setError] = useState(null); @@ -62,15 +64,21 @@ export const useStats = ({ ); if (!abortController.signal.aborted) { - setStats(response); + if (isMountedRef.current) { + setStats(response); + } } } catch (e) { if (!abortController.signal.aborted) { - setError(i18n.ERROR_LOADING_STATS(e.message)); + if (isMountedRef.current) { + setError(i18n.ERROR_LOADING_STATS(e.message)); + } } } finally { if (!abortController.signal.aborted) { - setLoading(false); + if (isMountedRef.current) { + setLoading(false); + } } } } @@ -80,7 +88,7 @@ export const useStats = ({ return () => { abortController.abort(); }; - }, [endDate, httpFetch, isILMAvailable, pattern, setError, startDate]); + }, [endDate, httpFetch, isILMAvailable, isMountedRef, pattern, setError, startDate]); return { stats, error, loading }; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/helpers.ts index a193456d4afa9..aa25a5fe00b44 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/helpers.ts @@ -85,11 +85,16 @@ export async function fetchUnallowedValues({ version: INTERNAL_API_VERSION, }); } catch (e) { - throw new Error( - i18n.ERROR_LOADING_UNALLOWED_VALUES({ - details: e.message, - indexName, - }) - ); + throw new UnallowedValuesError(indexName, e.message); + } +} + +export class UnallowedValuesError extends Error { + constructor(indexName: string, details: string) { + const message = i18n.ERROR_LOADING_UNALLOWED_VALUES({ details, indexName }); + super(message); + + this.name = 'UnallowedValuesError'; + this.message = message; } } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx index fcbb8aae9337f..d2d216fd12293 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx @@ -16,6 +16,7 @@ import { UnallowedValueRequestItem } from '../types'; import { useUnallowedValues, UseUnallowedValues } from '.'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { EcsFlatTyped } from '../constants'; +import { Theme } from '@elastic/charts'; const mockHttpFetch = jest.fn(); const mockReportDataQualityIndexChecked = jest.fn(); @@ -32,6 +33,45 @@ const ContextWrapper: FC> = ({ children }) => ( telemetryEvents={mockTelemetryEvents} isILMAvailable={true} toasts={toasts} + addSuccessToast={jest.fn()} + canUserCreateAndReadCases={jest.fn(() => true)} + endDate={null} + formatBytes={jest.fn()} + formatNumber={jest.fn()} + isAssistantEnabled={true} + lastChecked={'2023-03-28T22:27:28.159Z'} + openCreateCaseFlyout={jest.fn()} + patterns={['auditbeat-*']} + setLastChecked={jest.fn()} + startDate={null} + theme={{ + background: { + color: '#000', + }, + }} + baseTheme={ + { + background: { + color: '#000', + }, + } as Theme + } + ilmPhases={['hot', 'warm', 'unmanaged']} + selectedIlmPhaseOptions={[ + { + label: 'Hot', + value: 'hot', + }, + { + label: 'Warm', + value: 'warm', + }, + { + label: 'Unmanaged', + value: 'unmanaged', + }, + ]} + setSelectedIlmPhaseOptions={jest.fn()} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx index de0ce82fb8527..7f35da714ecd9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'; import { useDataQualityContext } from '../data_quality_panel/data_quality_context'; import { fetchUnallowedValues, getUnallowedValues } from './helpers'; import type { UnallowedValueCount, UnallowedValueRequestItem } from '../types'; +import { useIsMounted } from '../use_is_mounted'; export interface UseUnallowedValues { unallowedValues: Record | null; @@ -25,6 +26,7 @@ export const useUnallowedValues = ({ indexName: string; requestItems: UnallowedValueRequestItem[]; }): UseUnallowedValues => { + const { isMountedRef } = useIsMounted(); const [unallowedValues, setUnallowedValues] = useState { abortController.abort(); }; - }, [httpFetch, indexName, requestItems, setError]); + }, [httpFetch, indexName, isMountedRef, requestItems, setError]); return { unallowedValues, error, loading, requestTime }; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.test.ts new file mode 100644 index 0000000000000..9ea197360356f --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.test.ts @@ -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 { checkIndex, EMPTY_PARTITIONED_FIELD_METADATA } from './check_index'; +import { EMPTY_STAT } from '../helpers'; +import { mockMappingsResponse } from '../mock/mappings_response/mock_mappings_response'; +import { mockUnallowedValuesResponse } from '../mock/unallowed_values/mock_unallowed_values'; +import { UnallowedValueRequestItem, UnallowedValueSearchResult } from '../types'; +import { + getMappingsProperties, + getSortedPartitionedFieldMetadata, +} from '../data_quality_panel/index_properties/helpers'; +import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; +import { getUnallowedValues } from '../use_unallowed_values/helpers'; +import { getUnallowedValueRequestItems } from '../data_quality_panel/allowed_values/helpers'; +import { EcsFlatTyped } from '../constants'; + +let mockFetchMappings = jest.fn( + (_: { abortController: AbortController; patternOrIndexName: string }) => + Promise.resolve(mockMappingsResponse) +); + +jest.mock('../use_mappings/helpers', () => ({ + fetchMappings: ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + mockFetchMappings({ + abortController, + patternOrIndexName, + }), +})); + +const mockFetchUnallowedValues = jest.fn( + (_: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => Promise.resolve(mockUnallowedValuesResponse) +); + +jest.mock('../use_unallowed_values/helpers', () => { + const original = jest.requireActual('../use_unallowed_values/helpers'); + + return { + ...original, + fetchUnallowedValues: ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => + mockFetchUnallowedValues({ + abortController, + indexName, + requestItems, + }), + }; +}); + +describe('checkIndex', () => { + 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'; + const pattern = 'auditbeat-*'; + const httpFetch = jest.fn(); + + describe('when `checkIndex` successfully completes the check', () => { + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + }); + }); + + test('it invokes onCheckCompleted with a null `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with the non-default `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).not.toEqual( + EMPTY_PARTITIONED_FIELD_METADATA + ); + }); + + test('it invokes onCheckCompleted with the expected`pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + }); + + describe('lifecycle hooks', () => { + const orderOfCalls: string[] = []; + const onStart = jest.fn(() => orderOfCalls.push('onStart')); + const onSuccess = jest.fn(() => orderOfCalls.push('onSuccess')); + const onError = jest.fn(() => orderOfCalls.push('onError')); + const onLoadMappingsStart = jest.fn(() => orderOfCalls.push('onLoadMappingsStart')); + const onLoadMappingsSuccess = jest.fn(() => orderOfCalls.push('onLoadMappingsSuccess')); + const onLoadUnallowedValuesStart = jest.fn(() => + orderOfCalls.push('onLoadUnallowedValuesStart') + ); + const onLoadUnallowedValuesSuccess = jest.fn(() => + orderOfCalls.push('onLoadUnallowedValuesSuccess') + ); + + beforeEach(async () => { + orderOfCalls.length = 0; + jest.clearAllMocks(); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted: jest.fn(), + pattern, + onError, + onLoadMappingsStart, + onLoadMappingsSuccess, + onLoadUnallowedValuesStart, + onLoadUnallowedValuesSuccess, + onStart, + onSuccess, + }); + }); + + test('it invokes `onStart`', () => { + expect(onStart).toBeCalled(); + }); + + test('it invokes `onLoadMappingsStart`', () => { + expect(onLoadMappingsStart).toBeCalled(); + }); + + test('it invokes `onLoadMappingsSuccess` with mappings response', () => { + expect(onLoadMappingsSuccess).toBeCalledWith(mockMappingsResponse); + }); + + test('it invokes `onLoadUnallowedValuesStart`', () => { + expect(onLoadUnallowedValuesStart).toBeCalled(); + }); + + test('it invokes `onLoadUnallowedValuesSuccess` with unallowed value search results', () => { + expect(onLoadUnallowedValuesSuccess).toBeCalledWith(mockUnallowedValuesResponse); + }); + + test('it invokes `onSuccess` with the expected arguments', () => { + const mappingsProperties = getMappingsProperties({ + indexName, + indexes: mockMappingsResponse as Record, + }); + const unallowedValues = getUnallowedValues({ + requestItems: getUnallowedValueRequestItems({ + ecsMetadata: EcsFlatTyped, + indexName, + }), + searchResults: mockUnallowedValuesResponse as unknown as UnallowedValueSearchResult[], + }); + const partitionedFieldMetadata = getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties, + unallowedValues, + }); + expect(onSuccess).toBeCalledWith({ + partitionedFieldMetadata, + mappingsProperties, + unallowedValues, + }); + }); + + test('it does NOT invoke `onError`', () => { + expect(onError).not.toBeCalled(); + }); + + test('it invokes the lifecycle hooks in the expected order', () => { + expect(orderOfCalls).toEqual([ + 'onStart', + 'onLoadMappingsStart', + 'onLoadMappingsSuccess', + 'onLoadUnallowedValuesStart', + 'onLoadUnallowedValuesSuccess', + 'onSuccess', + ]); + }); + + describe('when load mappings error occurs', () => { + const error = 'simulated fetch mappings error'; + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + orderOfCalls.length = 0; + jest.clearAllMocks(); + + mockFetchMappings.mockRejectedValueOnce(new Error(error)); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + onError, + onLoadMappingsStart, + onLoadMappingsSuccess, + onLoadUnallowedValuesStart, + onLoadUnallowedValuesSuccess, + onStart, + onSuccess, + }); + }); + + test('it invokes `onError` with mappings error', () => { + expect(onError).toBeCalledWith(new Error(error)); + }); + + test('it does NOT invoke `onSuccess`', () => { + expect(onSuccess).not.toBeCalled(); + }); + + test('it invokes the lifecycle hooks in the expected order', () => { + expect(orderOfCalls).toEqual(['onStart', 'onLoadMappingsStart', 'onError']); + }); + }); + + describe('when load unallowed values error occurs', () => { + const error = 'simulated fetch unallowed values error'; + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + orderOfCalls.length = 0; + jest.clearAllMocks(); + + mockFetchUnallowedValues.mockRejectedValueOnce(new Error(error)); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + onError, + onLoadMappingsStart, + onLoadMappingsSuccess, + onLoadUnallowedValuesStart, + onLoadUnallowedValuesSuccess, + onStart, + onSuccess, + }); + }); + + test('it invokes `onError` with unallowed values error', () => { + expect(onError).toBeCalledWith(new Error(error)); + }); + + test('it does NOT invoke `onSuccess`', () => { + expect(onSuccess).not.toBeCalled(); + }); + + test('it invokes the lifecycle hooks in the expected order', () => { + expect(orderOfCalls).toEqual([ + 'onStart', + 'onLoadMappingsStart', + 'onLoadMappingsSuccess', + 'onLoadUnallowedValuesStart', + 'onError', + ]); + }); + }); + }); + + describe('happy path, when the signal is aborted', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it does NOT invoke onCheckCompleted', async () => { + const onCheckCompleted = jest.fn(); + + const abortController = new AbortController(); + abortController.abort(); + + await checkIndex({ + abortController, + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + }); + + expect(onCheckCompleted).not.toBeCalled(); + }); + }); + + describe('when an error occurs', () => { + const onCheckCompleted = jest.fn(); + const error = 'simulated fetch mappings error'; + + beforeEach(async () => { + jest.clearAllMocks(); + + mockFetchMappings = jest.fn( + (_: { abortController: AbortController; patternOrIndexName: string }) => + Promise.reject(new Error(error)) + ); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + }); + }); + + test('it invokes onCheckCompleted with the expected `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toEqual(error); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + }); + + describe('when an error occurs, but the error does not have a toString', () => { + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + mockFetchMappings = jest.fn( + (_: { abortController: AbortController; patternOrIndexName: string }) => + // eslint-disable-next-line prefer-promise-reject-errors + Promise.reject(undefined) + ); + + await checkIndex({ + abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + }); + }); + + test('it invokes onCheckCompleted with the fallback `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toEqual( + `An error occurred checking index ${indexName}` + ); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + }); + + describe('when an error occurs, and the signal is aborted', () => { + const onCheckCompleted = jest.fn(); + const abortController = new AbortController(); + abortController.abort(); + + const error = 'simulated fetch mappings error'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it does NOT invoke onCheckCompleted', async () => { + mockFetchMappings = jest.fn( + (_: { abortController: AbortController; patternOrIndexName: string }) => + Promise.reject(new Error(error)) + ); + + await checkIndex({ + abortController, + batchId: 'batch-id', + checkAllStartTime: Date.now(), + formatBytes, + formatNumber, + httpFetch, + indexName, + isLastCheck: false, + onCheckCompleted, + pattern, + }); + + expect(onCheckCompleted).not.toBeCalled(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.ts similarity index 50% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.ts index a9216b9d09fdd..8dd282c4121f0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/utils/check_index.ts @@ -6,16 +6,27 @@ */ import type { HttpHandler } from '@kbn/core-http-browser'; -import { getUnallowedValueRequestItems } from '../../../allowed_values/helpers'; +import { + IndicesGetMappingIndexMappingRecord, + MappingProperty, +} from '@elastic/elasticsearch/lib/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +import { getUnallowedValueRequestItems } from '../data_quality_panel/allowed_values/helpers'; import { getMappingsProperties, getSortedPartitionedFieldMetadata, -} from '../../../index_properties/helpers'; -import * as i18n from './translations'; -import type { OnCheckCompleted, PartitionedFieldMetadata } from '../../../../types'; -import { fetchMappings } from '../../../../use_mappings/helpers'; -import { fetchUnallowedValues, getUnallowedValues } from '../../../../use_unallowed_values/helpers'; -import { EcsFlatTyped } from '../../../../constants'; +} from '../data_quality_panel/index_properties/helpers'; +import * as i18n from '../data_quality_panel/data_quality_summary/summary_actions/check_all/translations'; +import type { + OnCheckCompleted, + PartitionedFieldMetadata, + UnallowedValueCount, + UnallowedValueSearchResult, +} from '../types'; +import { fetchMappings } from '../use_mappings/helpers'; +import { fetchUnallowedValues, getUnallowedValues } from '../use_unallowed_values/helpers'; +import { EcsFlatTyped } from '../constants'; export const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { all: [], @@ -25,46 +36,74 @@ export const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { sameFamily: [], }; +export interface CheckIndexProps { + abortController: AbortController; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + httpFetch: HttpHandler; + indexName: string; + onCheckCompleted: OnCheckCompleted; + pattern: string; + onLoadMappingsSuccess?: (indexes: Record) => void; + onLoadMappingsStart?: () => void; + onLoadUnallowedValuesSuccess?: (searchResults: UnallowedValueSearchResult[]) => void; + onLoadUnallowedValuesStart?: () => void; + onStart?: () => void; + onSuccess?: ({ + partitionedFieldMetadata, + mappingsProperties, + unallowedValues, + }: { + partitionedFieldMetadata: PartitionedFieldMetadata; + mappingsProperties: Record | null; + unallowedValues: Record; + }) => void; + onError?: (error: unknown) => void; + batchId?: string; + checkAllStartTime?: number; + isCheckAll?: boolean; + isLastCheck?: boolean; +} + export async function checkIndex({ abortController, - batchId, - checkAllStartTime, - ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, - isLastCheck, onCheckCompleted, pattern, - version, -}: { - abortController: AbortController; - batchId: string; - checkAllStartTime: number; - ecsMetadata: EcsFlatTyped; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - httpFetch: HttpHandler; - indexName: string; - isLastCheck: boolean; - onCheckCompleted: OnCheckCompleted; - pattern: string; - version: string; -}) { + onLoadMappingsSuccess, + onLoadMappingsStart, + onLoadUnallowedValuesSuccess, + onLoadUnallowedValuesStart, + onStart, + onSuccess, + onError, + isLastCheck = false, + batchId = uuidv4(), + checkAllStartTime = Date.now(), + isCheckAll = false, +}: CheckIndexProps) { try { const startTime = Date.now(); + + onStart?.(); + onLoadMappingsStart?.(); const indexes = await fetchMappings({ abortController, httpFetch, patternOrIndexName: indexName, }); + onLoadMappingsSuccess?.(indexes); + const requestItems = getUnallowedValueRequestItems({ - ecsMetadata, + ecsMetadata: EcsFlatTyped, indexName, }); + onLoadUnallowedValuesStart?.(); const searchResults = await fetchUnallowedValues({ abortController, httpFetch, @@ -72,6 +111,8 @@ export async function checkIndex({ requestItems, }); + onLoadUnallowedValuesSuccess?.(searchResults); + const unallowedValues = getUnallowedValues({ requestItems, searchResults, @@ -84,39 +125,47 @@ export async function checkIndex({ const partitionedFieldMetadata = getSortedPartitionedFieldMetadata({ - ecsMetadata, + ecsMetadata: EcsFlatTyped, loadingMappings: false, mappingsProperties, unallowedValues, }) ?? EMPTY_PARTITIONED_FIELD_METADATA; + onSuccess?.({ + partitionedFieldMetadata, + mappingsProperties, + unallowedValues, + }); if (!abortController.signal.aborted) { onCheckCompleted({ checkAllStartTime, batchId, error: null, formatBytes, + isCheckAll, formatNumber, indexName, partitionedFieldMetadata, pattern, requestTime: Date.now() - startTime, - version, isLastCheck, }); } - } catch (error) { + } catch (error: unknown) { + onError?.(error); + if (!abortController.signal.aborted) { onCheckCompleted({ checkAllStartTime, batchId, - error: error != null ? error.message : i18n.AN_ERROR_OCCURRED_CHECKING_INDEX(indexName), + error: + error instanceof Error ? error.message : i18n.AN_ERROR_OCCURRED_CHECKING_INDEX(indexName), formatBytes, formatNumber, + isCheckAll, indexName, partitionedFieldMetadata: null, pattern, - version, isLastCheck, }); } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/index.ts index 87c32ba78d5e6..5f9ab020ea21f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/index.ts @@ -17,6 +17,7 @@ export { ILM_PHASE, INDEX_LIFECYCLE_MANAGEMENT_PHASES, SELECT_ONE_OR_MORE_ILM_PHASES, + DATA_QUALITY_DASHBOARD_CONVERSATION_ID, } from './impl/data_quality/translations'; export { ECS_REFERENCE_URL } from './impl/data_quality/data_quality_panel/index_properties/markdown/helpers'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/jest.config.js b/x-pack/packages/security-solution/ecs_data_quality_dashboard/jest.config.js index e017b0ceaf369..5f6f7ab9bce1f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/jest.config.js +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/jest.config.js @@ -11,11 +11,11 @@ module.exports = { coverageReporters: ['text', 'html'], collectCoverageFrom: [ '/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/*.{ts,tsx}', - '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', - '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/*mock*.{ts,tsx}', - '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/*.test.{ts,tsx}', - '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/*.d.ts', - '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/*.config.ts', + '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/{__test__,__snapshots__,__examples__,*mock*,stub,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/*mock*.{ts,tsx}', + '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/*.test.{ts,tsx}', + '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/*.d.ts', + '!/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/**/*.config.ts', ], preset: '@kbn/test', rootDir: '../../../..', diff --git a/x-pack/plugins/security_solution/public/assistant/content/conversations/index.tsx b/x-pack/plugins/security_solution/public/assistant/content/conversations/index.tsx index 53fb26d205720..9234095990016 100644 --- a/x-pack/plugins/security_solution/public/assistant/content/conversations/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/content/conversations/index.tsx @@ -7,7 +7,8 @@ import { WELCOME_CONVERSATION_TITLE } from '@kbn/elastic-assistant'; import type { Conversation } from '@kbn/elastic-assistant'; -import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations'; +import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard'; + import { DETECTION_RULES_CONVERSATION_ID } from '../../../detections/pages/detection_engine/rules/translations'; import { ALERT_SUMMARY_CONVERSATION_ID, diff --git a/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.test.tsx b/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.test.tsx index 7fee44d0178b1..99836af47a7a8 100644 --- a/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.test.tsx +++ b/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.test.tsx @@ -8,10 +8,10 @@ import { renderHook } from '@testing-library/react-hooks'; import { useBaseConversations } from '.'; import { useLinkAuthorized } from '../../common/links'; import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; -import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations'; import { useKibana } from '../../common/lib/kibana'; import { BASE_SECURITY_CONVERSATIONS } from '../content/conversations'; import { unset } from 'lodash/fp'; +import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard'; const BASE_CONVERSATIONS_WITHOUT_DATA_QUALITY = unset( DATA_QUALITY_DASHBOARD_CONVERSATION_ID, diff --git a/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.tsx b/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.tsx index 3cc2d47682ec0..89fe80f4d29a3 100644 --- a/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/use_conversation_store/index.tsx @@ -8,8 +8,9 @@ import { type Conversation } from '@kbn/elastic-assistant'; import { unset } from 'lodash/fp'; -import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/translations'; import { useMemo } from 'react'; +import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '@kbn/ecs-data-quality-dashboard'; + import { BASE_SECURITY_CONVERSATIONS } from '../content/conversations'; import { useLinkAuthorized } from '../../common/links'; import { SecurityPageName } from '../../../common'; diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx index 16e610aa3f625..e39e2abd24169 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx @@ -12,7 +12,6 @@ import { MemoryRouter } from 'react-router-dom'; import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; import { TestProviders } from '../../common/mock'; import { DataQuality } from './data_quality'; -import { HOT, WARM, UNMANAGED } from './translations'; import { useKibana } from '../../common/lib/kibana'; const mockedUseKibana = mockUseKibana(); @@ -56,7 +55,7 @@ jest.mock('../../detections/containers/detection_engine/alerts/use_signal_index' })); describe('DataQuality', () => { - const defaultIlmPhases = `${HOT}${WARM}${UNMANAGED}`; + const defaultIlmPhases = 'hotwarmunmanaged'; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 50593a0569fcc..37fc927094993 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -10,29 +10,13 @@ import { DataQualityPanel, DATA_QUALITY_SUBTITLE, ECS_REFERENCE_URL, - getIlmPhaseDescription, - ILM_PHASE, - INDEX_LIFECYCLE_MANAGEMENT_PHASES, - SELECT_ONE_OR_MORE_ILM_PHASES, } from '@kbn/ecs-data-quality-dashboard'; -import type { EuiComboBoxOptionOption, OnTimeChangeProps } from '@elastic/eui'; -import { - EuiComboBox, - EuiFormControlLayout, - EuiFormLabel, - EuiLink, - EuiLoadingSpinner, - EuiText, - EuiToolTip, - useGeneratedHtmlId, - EuiSuperDatePicker, -} from '@elastic/eui'; +import type { OnTimeChangeProps } from '@elastic/eui'; +import { EuiLink, EuiLoadingSpinner, EuiText, EuiToolTip, EuiSuperDatePicker } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; import { useAssistantAvailability } from '../../assistant/use_assistant_availability'; import { SecurityPageName } from '../../app/types'; -import { getGroupByFieldsOnClick } from '../../detections/components/alerts_kpis/alerts_treemap_panel/alerts_treemap/lib/helpers'; import { useThemes } from '../../common/components/charts/common'; import { HeaderPage } from '../../common/components/header_page'; import { EmptyPrompt } from '../../common/components/empty_prompt'; @@ -51,85 +35,9 @@ import type { const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked'; -const comboBoxStyle: React.CSSProperties = { - width: '322px', -}; - -const FormControlLayout = styled(EuiFormControlLayout)` - max-width: 500px; - height: 42px; - - .euiFormControlLayout__childrenWrapper { - overflow: visible; - } -`; - -const Option = styled.div` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; -`; - -const OptionLabel = styled.span` - font-weight: bold; -`; - -const options: EuiComboBoxOptionOption[] = [ - { - label: i18n.HOT, - value: 'hot', - }, - { - label: i18n.WARM, - value: 'warm', - }, - { - disabled: true, - label: i18n.COLD, - value: 'cold', - }, - { - disabled: true, - label: i18n.FROZEN, - value: 'frozen', - }, - { - label: i18n.UNMANAGED, - value: 'unmanaged', - }, -]; - -const defaultOptions: EuiComboBoxOptionOption[] = [ - { - label: i18n.HOT, - value: 'hot', - }, - { - label: i18n.WARM, - value: 'warm', - }, - { - label: i18n.UNMANAGED, - value: 'unmanaged', - }, -]; - const DEFAULT_START_TIME = 'now-7d'; const DEFAULT_END_TIME = 'now'; -const renderOption = ( - option: EuiComboBoxOptionOption -): React.ReactNode => ( - - - -); - const DataQualityComponent: React.FC = () => { const { isAssistantEnabled } = useAssistantAvailability(); const httpFetch = KibanaServices.get().http.fetch; @@ -138,8 +46,6 @@ const DataQualityComponent: React.FC = () => { const [defaultBytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const labelInputId = useGeneratedHtmlId({ prefix: 'labelInput' }); - const [selectedOptions, setSelectedOptions] = useState(defaultOptions); const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView(); const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex(); const { configSettings, cases, telemetry } = useKibana().services; @@ -181,16 +87,6 @@ const DataQualityComponent: React.FC = () => { [] ); - const ilmPhases: string[] = useMemo( - () => selectedOptions.map(({ label }) => label), - [selectedOptions] - ); - - const ilmFormLabel = useMemo( - () => {ILM_PHASE}, - [labelInputId] - ); - const [lastChecked, setLastChecked] = useLocalStorage({ defaultValue: '', key: LOCAL_STORAGE_KEY, @@ -243,22 +139,6 @@ const DataQualityComponent: React.FC = () => { {indicesExist ? ( - {isILMAvailable && ( - - - - - - )} {!isILMAvailable && startDate && endDate && ( { defaultBytesFormat={defaultBytesFormat} defaultNumberFormat={defaultNumberFormat} endDate={endDate} - getGroupByFieldsOnClick={getGroupByFieldsOnClick} reportDataQualityCheckAllCompleted={reportDataQualityCheckAllCompleted} reportDataQualityIndexChecked={reportDataQualityIndexChecked} httpFetch={httpFetch} - ilmPhases={ilmPhases} isAssistantEnabled={isAssistantEnabled} isILMAvailable={isILMAvailable} lastChecked={lastChecked} diff --git a/x-pack/plugins/security_solution/public/overview/pages/translations.ts b/x-pack/plugins/security_solution/public/overview/pages/translations.ts index 54b82db440a90..e46bd59e1a632 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/pages/translations.ts @@ -18,10 +18,6 @@ export const BETA = i18n.translate('xpack.securitySolution.dataQualityDashboard. defaultMessage: 'Beta', }); -export const COLD = i18n.translate('xpack.securitySolution.overview.ilmPhaseCold', { - defaultMessage: 'cold', -}); - export const DATA_QUALITY_TITLE = i18n.translate( 'xpack.securitySolution.dataQualityDashboard.pageTitle', { @@ -56,14 +52,6 @@ export const EVENTS = i18n.translate('xpack.securitySolution.overview.eventsTitl defaultMessage: 'Event count', }); -export const FROZEN = i18n.translate('xpack.securitySolution.overview.ilmPhaseFrozen', { - defaultMessage: 'frozen', -}); - -export const HOT = i18n.translate('xpack.securitySolution.overview.ilmPhaseHot', { - defaultMessage: 'hot', -}); - export const NEWS_FEED_TITLE = i18n.translate( 'xpack.securitySolution.overview.newsFeedSidebarTitle', { @@ -88,10 +76,6 @@ export const TOP = (fieldName: string) => defaultMessage: `Top {fieldName}`, }); -export const UNMANAGED = i18n.translate('xpack.securitySolution.overview.ilmPhaseUnmanaged', { - defaultMessage: 'unmanaged', -}); - export const VIEW_ALERTS = i18n.translate('xpack.securitySolution.overview.viewAlertsButtonLabel', { defaultMessage: 'View alerts', }); @@ -100,10 +84,6 @@ export const VIEW_EVENTS = i18n.translate('xpack.securitySolution.overview.viewE defaultMessage: 'View events', }); -export const WARM = i18n.translate('xpack.securitySolution.overview.ilmPhaseWarm', { - defaultMessage: 'warm', -}); - export const DETECTION_RESPONSE_TITLE = i18n.translate( 'xpack.securitySolution.detectionResponse.pageTitle', { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c52ee4b30782b..6e59be904f43c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6838,8 +6838,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "Ajoutez ce rapport de Qualité des données comme contexte", "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "Expliquez les résultats ci-dessus et donnez des options pour résoudre les incompatibilités.", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "Vérifier les mappings d'index", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.chartTitle": "Mappings de champs", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.fieldsLabel": "Champs", "securitySolutionPackages.ecsDataQualityDashboard.ecsVersionStat": "Version ECS", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody": "Un problème est survenu lors du chargement des mappings : {error}", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle": "Impossible de charger les mappings d'index", @@ -6874,6 +6872,9 @@ "securitySolutionPackages.ecsDataQualityDashboard.getResultErrorTitle": "Erreur lors de la lecture des résultats d'examen qualité des données sauvegardées", "securitySolutionPackages.ecsDataQualityDashboard.hotDescription": "L'index est mis à jour et interrogé de façon active", "securitySolutionPackages.ecsDataQualityDashboard.hotPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"hot\". Les index \"hot\" sont mis à jour et interrogés de façon active.", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCold": "froid", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseFrozen": "frozen", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "hot", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "Phase ILM", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "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.ilmPhasesEmptyPromptColdLabel": "froid", @@ -6885,7 +6886,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "Sélectionner une ou plusieurs phases ILM", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "non géré", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "warm", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.dataQualityDashboardConversationId": "Tableau de bord de Qualité des données", + "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)", @@ -6938,13 +6940,10 @@ "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", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryTab": "Résumé", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel": "Inconnu", "securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip": "La taille de l'index principal (n'inclut pas de répliques)", "securitySolutionPackages.ecsDataQualityDashboard.lastCheckedLabel": "Dernière vérification", "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.allPassedTooltip": "Tous les index correspondant à ce modèle ont réussi les vérifications de qualité des données", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip": "Certains index correspondant à ce modèle ont échoué aux vérifications de qualité des données", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip": "La qualité des données n'a pas été vérifiée pour certains index correspondant à ce modèle", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.docsLabel": "Documents", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.indicesLabel": "Index", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip": "Modèle, ou index spécifique", @@ -6958,35 +6957,19 @@ "securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "Sélectionner un index pour le comparer à la version ECS", "securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "Sélectionner une ou plusieurs phases ILM", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "vérifié", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customIndexToolTip": "Décompte des mappings d'index personnalisés dans l'index {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customLabel": "Personnalisé", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customPatternToolTip": "Nombre total de mappings d'index personnalisés, dans les index correspondant au modèle {pattern}", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel": "Documents", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.fieldsLabel": "champs", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip": "Mappings et valeurs incompatibles avec ECS, dans l'index {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleLabel": "Incompatible", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatiblePatternToolTip": "Nombre total de champs incompatibles avec ECS, dans les index correspondant au modèle {pattern}", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsCountToolTip": "Nombre de documents dans l'index {indexName}", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsPatternToolTip": "Nombre total de documents, dans les index correspondant au modèle {pattern}", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel": "Index", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip": "Taille totale de tous les index principaux correspondant au modèle {pattern} (n'inclut pas les répliques)", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel": "Même famille", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyPatternToolTip": "Le nombre total de champs de la même famille tel que le type spécifié par l'ECS, dans les index correspondant au modèle {pattern}", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel": "Taille", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip": "Nombre total d'index vérifiés correspondant au modèle {pattern}", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesMatchingPatternToolTip": "Nombre total d'index correspondant au modèle {pattern}", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip": "Nombre total de documents, dans tous les index", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip": "Nombre total de champs incompatibles avec ECS, dans tous les index qui ont été vérifiés", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesCheckedToolTip": "Nombre total de tous les index vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip": "Nombre total de tous les index", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSameFamilyToolTip": "Nombre total de champs de la même famille que le type ECS, dans tous les index qui ont été vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip": "La taille totale de tous les index principaux (n'inclut pas de répliques)", "securitySolutionPackages.ecsDataQualityDashboard.storage.docs.unit": "{totalCount, plural, =1 {Document} other {Documents}}", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "Aucune donnée à afficher", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "Le champ {stackByField1} n'était présent dans aucun groupe", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "Réduire", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "Documents", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandLabel": "Développer", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "Développer les lignes", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "Échoué", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "Phase ILM", @@ -40259,11 +40242,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "Événements d'hôte", - "xpack.securitySolution.overview.ilmPhaseCold": "froid", - "xpack.securitySolution.overview.ilmPhaseFrozen": "frozen", - "xpack.securitySolution.overview.ilmPhaseHot": "hot", - "xpack.securitySolution.overview.ilmPhaseUnmanaged": "non géré", - "xpack.securitySolution.overview.ilmPhaseWarm": "warm", "xpack.securitySolution.overview.informationAriaLabel": "Informations", "xpack.securitySolution.overview.linkPanelLearnMoreButton": "En savoir plus", "xpack.securitySolution.overview.networkAction": "Afficher le réseau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d212feaf16ec6..4c7495ef5734f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6833,8 +6833,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "このデータ品質レポートをコンテキストとして追加", "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "上記の結果を説明し、一部のオプションを記述して非互換性を修正します。", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "インデックスマッピングの確認", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.chartTitle": "フィールドマッピング", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.fieldsLabel": "フィールド", "securitySolutionPackages.ecsDataQualityDashboard.ecsVersionStat": "ECSバージョン", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody": "マッピングの読み込み中に問題が発生しました:{error}", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle": "インデックスマッピングを読み込めません", @@ -6869,6 +6867,9 @@ "securitySolutionPackages.ecsDataQualityDashboard.getResultErrorTitle": "保存されたデータ品質チェック結果の読み取りエラー", "securitySolutionPackages.ecsDataQualityDashboard.hotDescription": "インデックスはアクティブに更新されており、照会されます", "securitySolutionPackages.ecsDataQualityDashboard.hotPatternTooltip": "{pattern}パターンと一致する{indices} {indices, plural, other {インデックス}}{indices, plural, other {は}}ホットです。ホットインデックスはアクティブに更新されており、照会されます。", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCold": "コールド", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseFrozen": "凍結", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "ホット", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "ILMフェーズ", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "これらのインデックスライフサイクル管理(ILM)フェーズのインデックスはデータ品質が確認されます", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel": "コールド", @@ -6880,7 +6881,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "1つ以上のILMフェーズを選択", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "管理対象外", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "ウォーム", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.dataQualityDashboardConversationId": "データ品質ダッシュボード", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "管理対象外", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "ウォーム", "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "非互換フィールドマッピング - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle": "非互換フィールド値 - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "これらのインデックスライフサイクル管理(ILM)フェーズのインデックスはデータ品質が確認されます", @@ -6933,13 +6935,10 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription": "場合によって、古い統合で作成されたインデックスには、以前あった互換性がなくなったマッピングまたは値が含まれることがあります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "`{indexName}`インデックスには、[Elastic Common Schema]({ecsReferenceUrl})(ECS)バージョン`{version}` [definitions]({ecsFieldReferenceUrl})とは異なる[マッピング]({mappingUrl})またはフィールド値があります。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "データ品質", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryTab": "まとめ", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel": "不明", "securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip": "プライマリインデックスのサイズ(レプリカは含まない)", "securitySolutionPackages.ecsDataQualityDashboard.lastCheckedLabel": "前回確認日時", "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.allPassedTooltip": "このパターンと一致するすべてのインデックスは、データ品質チェックに合格しました", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip": "このパターンと一致する一部のインデックスは、データ品質チェックに失敗しました", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip": "このパターンと一致する一部のインデックスは、データ品質が確認されませんでした", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.docsLabel": "ドキュメント", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.indicesLabel": "インデックス", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip": "パターンまたは特定のインデックス", @@ -6953,35 +6952,19 @@ "securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "ECSバージョンと比較するインデックスを選択", "securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "1つ以上のILMフェーズを選択", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "確認済み", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customIndexToolTip": "{indexName}インデックスのカスタムフィールドマッピングの件数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customLabel": "カスタム", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customPatternToolTip": "{pattern}パターンと一致するインデックスのカスタムフィールドマッピングの合計件数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel": "ドキュメント", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.fieldsLabel": "フィールド", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip": "{indexName}インデックスのESCと互換性があるマッピングと値", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleLabel": "非互換", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatiblePatternToolTip": "{pattern}パターンと一致するインデックスのECSと互換性があるフィールドの合計件数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsCountToolTip": "{indexName}インデックスのドキュメントの件数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsPatternToolTip": "{pattern}パターンと一致するインデックスのドキュメントの合計件数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel": "インデックス", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip": "{pattern}パターンと一致するプライマリインデックスの合計サイズ(レプリカを含まない)", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel": "同じファミリー", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyPatternToolTip": "ECSで指定された型と同じファミリーに属する、{pattern}パターンと一致するインデックスのフィールドの総数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel": "サイズ", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip": "{pattern}パターンと一致する確認されたインデックスの合計件数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesMatchingPatternToolTip": "{pattern}パターンと一致するインデックスの合計件数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip": "すべてのインデックスのドキュメントの合計数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip": "確認されたすべてのインデックスのECSと互換性がないフィールドの合計件数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesCheckedToolTip": "確認されたすべてのインデックスの合計数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip": "すべてのインデックスの合計数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSameFamilyToolTip": "確認されたすべてのインデックスにおける、ECSの型と同じファミリーのフィールドの総数。", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip": "すべてのプライマリインデックスの合計サイズ(レプリカを含まない)", "securitySolutionPackages.ecsDataQualityDashboard.storage.docs.unit": "{totalCount, plural, other {ドキュメント}}", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "表示するデータがありません", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "{stackByField1}フィールドがどのグループにも存在しませんでした", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "縮小", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "ドキュメント", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandLabel": "拡張", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "行を展開", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "失敗", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILMフェーズ", @@ -40242,11 +40225,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "ホストイベント", - "xpack.securitySolution.overview.ilmPhaseCold": "コールド", - "xpack.securitySolution.overview.ilmPhaseFrozen": "凍結", - "xpack.securitySolution.overview.ilmPhaseHot": "ホット", - "xpack.securitySolution.overview.ilmPhaseUnmanaged": "管理対象外", - "xpack.securitySolution.overview.ilmPhaseWarm": "ウォーム", "xpack.securitySolution.overview.informationAriaLabel": "情報", "xpack.securitySolution.overview.linkPanelLearnMoreButton": "詳細情報", "xpack.securitySolution.overview.networkAction": "ネットワークを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0c8effa03f31d..0928a41fb596a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6844,8 +6844,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "将此数据质量报告添加为上下文", "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "解释上述结果,并说明某些选项以解决不兼容问题。", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "检查索引映射", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.chartTitle": "字段映射", - "securitySolutionPackages.ecsDataQualityDashboard.ecsSummaryDonutChart.fieldsLabel": "字段", "securitySolutionPackages.ecsDataQualityDashboard.ecsVersionStat": "ECS 版本", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody": "加载映射时出现问题:{error}", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle": "无法加载索引映射", @@ -6880,6 +6878,9 @@ "securitySolutionPackages.ecsDataQualityDashboard.getResultErrorTitle": "读取保存的数据质量检查结果时出错", "securitySolutionPackages.ecsDataQualityDashboard.hotDescription": "该索引会被主动地更新和查询", "securitySolutionPackages.ecsDataQualityDashboard.hotPatternTooltip": "{indices} 个匹配 {pattern} 模式的{indices, plural, other {索引}}{indices, plural, other {为}}热索引。热索引会被主动地更新和查询。", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCold": "冷", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseFrozen": "冻结", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "热", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "ILM 阶段", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "将检查具有这些索引生命周期管理 (ILM) 阶段的索引以了解数据质量", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel": "冷", @@ -6891,7 +6892,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "选择一个或多个 ILM 阶段", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "未受管", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "温", - "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.dataQualityDashboardConversationId": "数据质量仪表板", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "未受管", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "温", "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "不兼容的字段映射 - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldValuesTableTitle": "不兼容的字段值 - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "将检查具有这些索引生命周期管理 (ILM) 阶段的索引以了解数据质量", @@ -6944,13 +6946,10 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.sometimesIndicesCreatedByOlderDescription": "有时候,用较旧集成创建的索引的映射或值可能过去符合规范,但现在不再符合。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "`{indexName}` 索引具有与 [Elastic Common Schema] ({ecsReferenceUrl}) (ECS) 版本 `{version}` [定义]({ecsFieldReferenceUrl}) 不同的[映射]({mappingUrl}) 或字段值。", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "数据质量", - "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryTab": "摘要", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel": "未知", "securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip": "主要索引(不包括副本)的大小", "securitySolutionPackages.ecsDataQualityDashboard.lastCheckedLabel": "上次检查时间", "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.allPassedTooltip": "与此模式匹配的所有索引均通过了数据质量检查", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip": "与此模式匹配的某些索引未通过数据质量检查", - "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip": "与此模式匹配的某些索引尚未进行数据质量检查", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.docsLabel": "文档", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.indicesLabel": "索引", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip": "模式或特定索引", @@ -6964,35 +6963,19 @@ "securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "选择索引以将其与 ECS 版本进行比较", "securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "选择一个或多个 ILM 阶段", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "已检查", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customIndexToolTip": "{indexName} 索引中定制字段映射的计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customLabel": "定制", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.customPatternToolTip": "与 {pattern} 模式匹配的索引中定制字段映射的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel": "文档", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.fieldsLabel": "字段", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip": "{indexName} 索引中与 ECS 不兼容的映射和值", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleLabel": "不兼容", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatiblePatternToolTip": "与 {pattern} 模式匹配的索引中与 ECS 不兼容的字段的总计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsCountToolTip": "{indexName} 索引中的文档计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsPatternToolTip": "与 {pattern} 模式匹配的索引中文档的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel": "索引", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip": "与 {pattern} 模式匹配的主要索引(不包括副本)的总大小", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel": "同一系列", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyPatternToolTip": "与 {pattern} 模式匹配的索引中类型由 ECS 指定的同一系列中的字段总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel": "大小", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip": "经检查与 {pattern} 模式匹配的索引的总计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesMatchingPatternToolTip": "与 {pattern} 模式匹配的索引的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip": "所有索引中文档的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip": "检查的所有索引中与 ECS 不兼容的字段的总计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesCheckedToolTip": "检查的所有索引的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip": "所有索引的总计数", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSameFamilyToolTip": "检查的所有索引中与 ECS 类型相同的系列中字段的总计数", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip": "所有主要索引(不包括副本)的总大小", "securitySolutionPackages.ecsDataQualityDashboard.storage.docs.unit": "{totalCount, plural, other {个文档}}", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "没有可显示的数据", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "任何组中都不存在 {stackByField1} 字段", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "折叠", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "文档", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandLabel": "展开", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "展开行", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "失败", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILM 阶段", @@ -40285,11 +40268,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "主机事件", - "xpack.securitySolution.overview.ilmPhaseCold": "冷", - "xpack.securitySolution.overview.ilmPhaseFrozen": "冻结", - "xpack.securitySolution.overview.ilmPhaseHot": "热", - "xpack.securitySolution.overview.ilmPhaseUnmanaged": "未受管", - "xpack.securitySolution.overview.ilmPhaseWarm": "温", "xpack.securitySolution.overview.informationAriaLabel": "信息", "xpack.securitySolution.overview.linkPanelLearnMoreButton": "了解详情", "xpack.securitySolution.overview.networkAction": "查看网络",