diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/constants.ts index 26fe2698f077c..6e4649e9e8e74 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/constants.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/constants.ts @@ -8,7 +8,7 @@ import { EcsFlat } from '@elastic/ecs'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EcsFieldMetadata } from './types'; +import { EcsFieldMetadata, PartitionedFieldMetadata, SortConfig } from './types'; import * as i18n from './translations'; export const EcsFlatTyped = EcsFlat as unknown as Record; @@ -42,3 +42,18 @@ export const ilmPhaseOptionsStatic: EuiComboBoxOptionOption[] = [ export const EMPTY_STAT = '--'; export const INTERNAL_API_VERSION = '1'; + +export const defaultSort: SortConfig = { + sort: { + direction: 'desc', + field: 'docsCount', + }, +}; + +export const EMPTY_METADATA: PartitionedFieldMetadata = { + all: [], + ecsCompliant: [], + custom: [], + incompatible: [], + sameFamily: [], +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts new file mode 100644 index 0000000000000..634f2cd8f59cc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MIN_PAGE_SIZE = 10; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.test.ts deleted file mode 100644 index 56dc6364ba845..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.test.ts +++ /dev/null @@ -1,1017 +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 { - IlmExplainLifecycleLifecycleExplain, - IlmExplainLifecycleLifecycleExplainManaged, - IlmExplainLifecycleLifecycleExplainUnmanaged, -} from '@elastic/elasticsearch/lib/api/types'; - -import { - defaultSort, - getIlmPhase, - getIndexPropertiesContainerId, - getIlmExplainPhaseCounts, - getIndexIncompatible, - getPageIndex, - getPhaseCount, - getSummaryTableItems, - isManaged, - shouldCreateIndexNames, - shouldCreatePatternRollup, -} from './helpers'; -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 { DataQualityCheckResult } from '../../../types'; -import { IndexSummaryTableItem } from './types'; -import { getIndexNames, getPatternDocsCount } from './utils/stats'; - -const hot: IlmExplainLifecycleLifecycleExplainManaged = { - index: '.ds-packetbeat-8.6.1-2023.02.04-000001', - managed: true, - policy: 'packetbeat', - index_creation_date_millis: 1675536751379, - time_since_index_creation: '3.98d', - lifecycle_date_millis: 1675536751379, - age: '3.98d', - phase: 'hot', - phase_time_millis: 1675536751809, - action: 'rollover', - action_time_millis: 1675536751809, - step: 'check-rollover-ready', - step_time_millis: 1675536751809, - phase_execution: { - policy: 'packetbeat', - version: 1, - modified_date_in_millis: 1675536751205, - }, -}; -const warm = { - ...hot, - phase: 'warm', -}; -const cold = { - ...hot, - phase: 'cold', -}; -const frozen = { - ...hot, - phase: 'frozen', -}; -const other = { - ...hot, - phase: 'other', // not a valid phase -}; - -const managed: Record = { - hot, - warm, - cold, - frozen, -}; - -const unmanaged: IlmExplainLifecycleLifecycleExplainUnmanaged = { - index: 'michael', - managed: false, -}; - -describe('helpers', () => { - const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; - - describe('isManaged', () => { - test('it returns true when the `ilmExplainRecord` `managed` property is true', () => { - const ilmExplain = mockIlmExplain[indexName]; - - expect(isManaged(ilmExplain)).toBe(true); - }); - - test('it returns false when the `ilmExplainRecord` is undefined', () => { - expect(isManaged(undefined)).toBe(false); - }); - }); - - describe('getPhaseCount', () => { - test('it returns the expected count when an index with the specified `ilmPhase` exists in the `IlmExplainLifecycleLifecycleExplain` record', () => { - expect( - getPhaseCount({ - ilmExplain: mockIlmExplain, - ilmPhase: 'hot', // this phase is in the record - indexName, // valid index name - }) - ).toEqual(1); - }); - - test('it returns zero when `ilmPhase` is null', () => { - expect( - getPhaseCount({ - ilmExplain: null, - ilmPhase: 'hot', - indexName, - }) - ).toEqual(0); - }); - - test('it returns zero when the `indexName` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { - expect( - getPhaseCount({ - ilmExplain: mockIlmExplain, - ilmPhase: 'hot', - indexName: 'invalid', // this index does NOT exist - }) - ).toEqual(0); - }); - - test('it returns zero when the specified `ilmPhase` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { - expect( - getPhaseCount({ - ilmExplain: mockIlmExplain, - ilmPhase: 'warm', // this phase is NOT in the record - indexName, // valid index name - }) - ).toEqual(0); - }); - - describe('when `ilmPhase` is `unmanaged`', () => { - test('it returns the expected count for an `unmanaged` index', () => { - const index = 'auditbeat-custom-index-1'; - const ilmExplainRecord: IlmExplainLifecycleLifecycleExplain = { - index, - managed: false, - }; - const ilmExplain = { - [index]: ilmExplainRecord, - }; - - expect( - getPhaseCount({ - ilmExplain, - ilmPhase: 'unmanaged', // ilmPhase is unmanaged - indexName: index, // an unmanaged index - }) - ).toEqual(1); - }); - - test('it returns zero for a managed index', () => { - expect( - getPhaseCount({ - ilmExplain: mockIlmExplain, - ilmPhase: 'unmanaged', // ilmPhase is unmanaged - indexName, // a managed (`hot`) index - }) - ).toEqual(0); - }); - }); - }); - - describe('getIlmPhase', () => { - const isILMAvailable = true; - test('it returns undefined when the `ilmExplainRecord` is undefined', () => { - expect(getIlmPhase(undefined, isILMAvailable)).toBeUndefined(); - }); - - describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainManaged` record', () => { - Object.keys(managed).forEach((phase) => - test(`it returns the expected phase when 'phase' is '${phase}'`, () => { - expect(getIlmPhase(managed[phase], isILMAvailable)).toEqual(phase); - }) - ); - - test(`it returns undefined when the 'phase' is unknown`, () => { - expect(getIlmPhase(other, isILMAvailable)).toBeUndefined(); - }); - }); - - describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainUnmanaged` record', () => { - test('it returns `unmanaged`', () => { - expect(getIlmPhase(unmanaged, isILMAvailable)).toEqual('unmanaged'); - }); - }); - }); - - describe('getIlmExplainPhaseCounts', () => { - test('it returns the expected counts (all zeros) when `ilmExplain` is null', () => { - expect(getIlmExplainPhaseCounts(null)).toEqual({ - cold: 0, - frozen: 0, - hot: 0, - unmanaged: 0, - warm: 0, - }); - }); - - test('it returns the expected counts', () => { - const ilmExplain: Record = { - ...managed, - [unmanaged.index]: unmanaged, - }; - - expect(getIlmExplainPhaseCounts(ilmExplain)).toEqual({ - cold: 1, - frozen: 1, - hot: 1, - unmanaged: 1, - warm: 1, - }); - }); - }); - - describe('getIndexIncompatible', () => { - test('it returns undefined when `results` is undefined', () => { - expect( - getIndexIncompatible({ - indexName, - results: undefined, // <-- - }) - ).toBeUndefined(); - }); - - test('it returns undefined when `indexName` is not in the `results`', () => { - expect( - getIndexIncompatible({ - indexName: 'not_in_the_results', // <-- - results: mockDataQualityCheckResult, - }) - ).toBeUndefined(); - }); - - test('it returns the expected count', () => { - expect( - getIndexIncompatible({ - indexName: 'auditbeat-custom-index-1', - results: mockDataQualityCheckResult, - }) - ).toEqual(3); - }); - }); - - describe('getSummaryTableItems', () => { - const indexNames = [ - '.ds-packetbeat-8.6.1-2023.02.04-000001', - '.ds-packetbeat-8.5.3-2023.02.04-000001', - 'auditbeat-custom-index-1', - ]; - const pattern = 'auditbeat-*'; - const patternDocsCount = 4; - const results: Record = { - 'auditbeat-custom-index-1': { - docsCount: 4, - error: null, - ilmPhase: 'unmanaged', - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - markdownComments: [ - '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` |\n\n', - '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', - "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\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),\n`theory` (1) |\n\n', - ], - pattern: 'auditbeat-*', - sameFamily: 0, - checkedAt: 1706526408000, - }, - }; - const isILMAvailable = true; - - test('it returns the expected summary table items', () => { - expect( - getSummaryTableItems({ - ilmExplain: mockIlmExplain, - indexNames, - isILMAvailable, - pattern, - patternDocsCount, - results, - sortByColumn: defaultSort.sort.field, - sortByDirection: defaultSort.sort.direction, - stats: mockStats, - }) - ).toEqual([ - { - docsCount: 1630289, - ilmPhase: 'hot', - incompatible: undefined, - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 733175040, - checkedAt: undefined, - }, - { - docsCount: 1628343, - ilmPhase: 'hot', - incompatible: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 731583142, - checkedAt: undefined, - }, - { - docsCount: 4, - ilmPhase: 'unmanaged', - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 28413, - checkedAt: 1706526408000, - }, - ]); - }); - - test('it returns the expected summary table items when isILMAvailable is false', () => { - expect( - getSummaryTableItems({ - ilmExplain: mockIlmExplain, - indexNames, - isILMAvailable: false, - pattern, - patternDocsCount, - results, - sortByColumn: defaultSort.sort.field, - sortByDirection: defaultSort.sort.direction, - stats: mockStats, - }) - ).toEqual([ - { - docsCount: 1630289, - ilmPhase: undefined, - incompatible: undefined, - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 733175040, - checkedAt: undefined, - }, - { - docsCount: 1628343, - ilmPhase: undefined, - incompatible: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 731583142, - checkedAt: undefined, - }, - { - docsCount: 4, - ilmPhase: undefined, - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 28413, - checkedAt: 1706526408000, - }, - ]); - }); - - test('it returns the expected summary table items when `sortByDirection` is ascending', () => { - expect( - getSummaryTableItems({ - ilmExplain: mockIlmExplain, - indexNames, - isILMAvailable, - pattern, - patternDocsCount, - results, - sortByColumn: defaultSort.sort.field, - sortByDirection: 'asc', // <-- ascending - stats: mockStats, - }) - ).toEqual([ - { - docsCount: 4, - ilmPhase: 'unmanaged', - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 28413, - checkedAt: 1706526408000, - }, - { - docsCount: 1628343, - ilmPhase: 'hot', - incompatible: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 731583142, - checkedAt: undefined, - }, - { - docsCount: 1630289, - ilmPhase: 'hot', - incompatible: undefined, - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: 733175040, - checkedAt: undefined, - }, - ]); - }); - - test('it returns the expected summary table items when data is unavailable', () => { - expect( - getSummaryTableItems({ - ilmExplain: null, // <-- no data - indexNames, - isILMAvailable, - pattern, - patternDocsCount, - results: undefined, // <-- no data - sortByColumn: defaultSort.sort.field, - sortByDirection: defaultSort.sort.direction, - stats: null, // <-- no data - }) - ).toEqual([ - { - docsCount: 0, - ilmPhase: undefined, - incompatible: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: undefined, - checkedAt: undefined, - }, - { - docsCount: 0, - ilmPhase: undefined, - incompatible: undefined, - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: undefined, - checkedAt: undefined, - }, - { - docsCount: 0, - ilmPhase: undefined, - incompatible: undefined, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - patternDocsCount: 4, - sizeInBytes: undefined, - checkedAt: undefined, - }, - ]); - }); - }); - - describe('shouldCreateIndexNames', () => { - const indexNames = [ - '.ds-packetbeat-8.6.1-2023.02.04-000001', - '.ds-packetbeat-8.5.3-2023.02.04-000001', - 'auditbeat-custom-index-1', - ]; - const isILMAvailable = true; - - test('returns true when `indexNames` does NOT exist, and the required `stats` and `ilmExplain` are available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: mockIlmExplain, - indexNames: undefined, - isILMAvailable, - newIndexNames: [], - stats: mockStats, - }) - ).toBe(true); - }); - - test('returns true when `isILMAvailable` is false, and the required `stats` is available, and `ilmExplain` is not available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: null, - indexNames: undefined, - isILMAvailable: false, - newIndexNames: [], - stats: mockStats, - }) - ).toBe(true); - }); - - test('returns false when `indexNames` exists, and the required `stats` and `ilmExplain` are available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: mockIlmExplain, - indexNames, - isILMAvailable, - newIndexNames: indexNames, - stats: mockStats, - }) - ).toBe(false); - }); - - test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: mockIlmExplain, - indexNames: undefined, - isILMAvailable, - newIndexNames: [], - stats: null, - }) - ).toBe(false); - }); - - test('returns false when `indexNames` does NOT exist, `stats` is available, and `ilmExplain` is NOT available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: null, - indexNames: undefined, - isILMAvailable, - newIndexNames: [], - stats: mockStats, - }) - ).toBe(false); - }); - - test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is NOT available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: null, - indexNames: undefined, - isILMAvailable, - newIndexNames: [], - stats: null, - }) - ).toBe(false); - }); - - test('returns false when `indexNames` exists, `stats` is NOT available, and `ilmExplain` is NOT available', () => { - expect( - shouldCreateIndexNames({ - ilmExplain: null, - indexNames, - isILMAvailable, - newIndexNames: [], - stats: null, - }) - ).toBe(false); - }); - }); - - describe('shouldCreatePatternRollup', () => { - const isILMAvailable = true; - const newIndexNames = getIndexNames({ - stats: mockStats, - ilmExplain: mockIlmExplain, - ilmPhases: ['hot', 'unmanaged'], - isILMAvailable, - }); - const newDocsCount = getPatternDocsCount({ indexNames: newIndexNames, stats: mockStats }); - test('it returns false when the `patternRollup.docsCount` equals newDocsCount', () => { - expect( - shouldCreatePatternRollup({ - error: null, - ilmExplain: mockIlmExplain, - isILMAvailable, - newDocsCount: auditbeatWithAllResults.docsCount as number, - patternRollup: auditbeatWithAllResults, - stats: mockStats, - }) - ).toBe(false); - }); - - test('it returns true when all data and ILMExplain were loaded', () => { - expect( - shouldCreatePatternRollup({ - error: null, - ilmExplain: mockIlmExplain, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: mockStats, - }) - ).toBe(true); - }); - - test('it returns true when all data was loaded and ILM is not available', () => { - expect( - shouldCreatePatternRollup({ - error: null, - ilmExplain: null, - isILMAvailable: false, - newDocsCount, - patternRollup: undefined, - stats: mockStats, - }) - ).toBe(true); - }); - - test('it returns false when `stats`, but NOT `ilmExplain` was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: null, - ilmExplain: null, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: mockStats, - }) - ).toBe(false); - }); - - test('it returns false when `stats` was NOT loaded, and `ilmExplain` was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: null, - ilmExplain: mockIlmExplain, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: null, - }) - ).toBe(false); - }); - - test('it returns true if an error occurred, and NO data was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: 'whoops', - ilmExplain: null, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: null, - }) - ).toBe(true); - }); - - test('it returns true if an error occurred, and just `stats` was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: 'something went', - ilmExplain: null, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: mockStats, - }) - ).toBe(true); - }); - - test('it returns true if an error occurred, and just `ilmExplain` was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: 'horribly wrong', - ilmExplain: mockIlmExplain, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: null, - }) - ).toBe(true); - }); - - test('it returns true if an error occurred, and all data was loaded', () => { - expect( - shouldCreatePatternRollup({ - error: 'over here', - ilmExplain: mockIlmExplain, - isILMAvailable, - newDocsCount, - patternRollup: undefined, - stats: mockStats, - }) - ).toBe(true); - }); - }); - - describe('getIndexPropertiesContainerId', () => { - const pattern = 'auditbeat-*'; - - test('it returns the expected id', () => { - expect(getIndexPropertiesContainerId({ indexName, pattern })).toEqual( - 'index-properties-container-auditbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001' - ); - }); - }); - - describe('getPageIndex', () => { - const getPageIndexArgs: { - indexName: string; - items: IndexSummaryTableItem[]; - pageSize: number; - } = { - indexName: 'auditbeat-7.17.9-2023.04.09-000001', // <-- on page 2 of 3 (page index 1) - items: [ - { - docsCount: 48077, - incompatible: undefined, - indexName: 'auditbeat-7.14.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 43357342, - checkedAt: 1706526408000, - }, - { - docsCount: 48068, - incompatible: undefined, - indexName: 'auditbeat-7.3.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 32460397, - checkedAt: 1706526408000, - }, - { - docsCount: 48064, - incompatible: undefined, - indexName: 'auditbeat-7.11.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 42782794, - checkedAt: 1706526408000, - }, - { - docsCount: 47868, - incompatible: undefined, - indexName: 'auditbeat-7.6.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 31575964, - checkedAt: 1706526408000, - }, - { - docsCount: 47827, - incompatible: 20, - indexName: 'auditbeat-7.15.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 44130657, - checkedAt: 1706526408000, - }, - { - docsCount: 47642, - incompatible: undefined, - indexName: '.ds-auditbeat-8.4.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 42412521, - checkedAt: 1706526408000, - }, - { - docsCount: 47545, - incompatible: undefined, - indexName: 'auditbeat-7.16.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41423244, - checkedAt: 1706526408000, - }, - { - docsCount: 47531, - incompatible: undefined, - indexName: 'auditbeat-7.5.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 32394133, - checkedAt: 1706526408000, - }, - { - docsCount: 47530, - incompatible: undefined, - indexName: 'auditbeat-7.12.1-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 43015519, - checkedAt: 1706526408000, - }, - { - docsCount: 47520, - incompatible: undefined, - indexName: '.ds-auditbeat-8.0.1-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 42230604, - checkedAt: 1706526408000, - }, - { - docsCount: 47496, - incompatible: undefined, - indexName: '.ds-auditbeat-8.2.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41710968, - checkedAt: 1706526408000, - }, - { - docsCount: 47486, - incompatible: undefined, - indexName: '.ds-auditbeat-8.5.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 42295944, - checkedAt: 1706526408000, - }, - { - docsCount: 47486, - incompatible: undefined, - indexName: '.ds-auditbeat-8.3.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41761321, - checkedAt: 1706526408000, - }, - { - docsCount: 47460, - incompatible: undefined, - indexName: 'auditbeat-7.2.1-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 30481198, - checkedAt: 1706526408000, - }, - { - docsCount: 47439, - incompatible: undefined, - indexName: 'auditbeat-7.17.9-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41554041, - checkedAt: 1706526408000, - }, - { - docsCount: 47395, - incompatible: undefined, - indexName: 'auditbeat-7.9.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 42815907, - checkedAt: 1706526408000, - }, - { - docsCount: 47394, - incompatible: undefined, - indexName: '.ds-auditbeat-8.7.0-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41157112, - checkedAt: 1706526408000, - }, - { - docsCount: 47372, - incompatible: undefined, - indexName: 'auditbeat-7.4.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 31626792, - checkedAt: 1706526408000, - }, - { - docsCount: 47369, - incompatible: undefined, - indexName: 'auditbeat-7.13.4-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41828969, - checkedAt: 1706526408000, - }, - { - docsCount: 47348, - incompatible: undefined, - indexName: 'auditbeat-7.7.1-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 40010773, - checkedAt: 1706526408000, - }, - { - docsCount: 47339, - incompatible: undefined, - indexName: 'auditbeat-7.10.2-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 43480570, - checkedAt: 1706526408000, - }, - { - docsCount: 47325, - incompatible: undefined, - indexName: '.ds-auditbeat-8.1.3-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 41822475, - checkedAt: 1706526408000, - }, - { - docsCount: 47294, - incompatible: undefined, - indexName: 'auditbeat-7.8.0-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 43018490, - checkedAt: 1706526408000, - }, - { - docsCount: 24276, - incompatible: undefined, - indexName: '.ds-auditbeat-8.6.1-2023.04.09-000001', - ilmPhase: 'hot', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 23579440, - checkedAt: 1706526408000, - }, - { - docsCount: 4, - incompatible: undefined, - indexName: 'auditbeat-custom-index-1', - ilmPhase: 'unmanaged', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 28409, - checkedAt: 1706526408000, - }, - { - docsCount: 0, - incompatible: undefined, - indexName: 'auditbeat-custom-empty-index-1', - ilmPhase: 'unmanaged', - pattern: 'auditbeat-*', - patternDocsCount: 1118155, - sizeInBytes: 247, - checkedAt: 1706526408000, - }, - ], - pageSize: 10, - }; - - test('it returns the expected page index', () => { - expect(getPageIndex(getPageIndexArgs)).toEqual(1); - }); - - test('it returns the expected page index for the first item', () => { - const firstItemIndexName = 'auditbeat-7.14.2-2023.04.09-000001'; - - expect( - getPageIndex({ - ...getPageIndexArgs, - indexName: firstItemIndexName, - }) - ).toEqual(0); - }); - - test('it returns the expected page index for the last item', () => { - const lastItemIndexName = 'auditbeat-custom-empty-index-1'; - - expect( - getPageIndex({ - ...getPageIndexArgs, - indexName: lastItemIndexName, - }) - ).toEqual(2); - }); - - test('it returns null when the index cannot be found', () => { - expect( - getPageIndex({ - ...getPageIndexArgs, - indexName: 'does_not_exist', // <-- this index is not in the items - }) - ).toBeNull(); - }); - - test('it returns null when `pageSize` is zero', () => { - expect( - getPageIndex({ - ...getPageIndexArgs, - pageSize: 0, // <-- invalid - }) - ).toBeNull(); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.ts deleted file mode 100644 index 5470854fd2ff0..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/helpers.ts +++ /dev/null @@ -1,258 +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 { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; -import { isEqual, orderBy } from 'lodash/fp'; - -import type { - IlmPhase, - IlmExplainPhaseCounts, - DataQualityCheckResult, - PatternRollup, - SortConfig, - MeteringStatsIndex, -} from '../../../types'; -import { IndexSummaryTableItem } from './types'; -import { getDocsCount, getSizeInBytes } from '../../../utils/stats'; - -export const isManaged = ( - ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined -): boolean => ilmExplainRecord?.managed === true; - -export const getPhaseCount = ({ - ilmExplain, - ilmPhase, - indexName, -}: { - ilmExplain: Record | null; - ilmPhase: IlmPhase; - indexName: string; -}): number => { - const ilmExplainRecord = ilmExplain != null ? ilmExplain[indexName] : undefined; - - if (ilmPhase === 'unmanaged') { - return isManaged(ilmExplainRecord) ? 0 : 1; - } else if (ilmExplainRecord != null && 'phase' in ilmExplainRecord) { - return ilmExplainRecord.phase === ilmPhase ? 1 : 0; - } - - return 0; -}; - -export const getIlmPhase = ( - ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined, - isILMAvailable: boolean -): IlmPhase | undefined => { - if (ilmExplainRecord == null || !isILMAvailable) { - return undefined; - } - - if ('phase' in ilmExplainRecord) { - const phase = ilmExplainRecord.phase; - - switch (phase) { - case 'hot': - return 'hot'; - case 'warm': - return 'warm'; - case 'cold': - return 'cold'; - case 'frozen': - return 'frozen'; - default: - return undefined; - } - } else { - return 'unmanaged'; - } -}; - -export const getIlmExplainPhaseCounts = ( - ilmExplain: Record | null -): IlmExplainPhaseCounts => { - const indexNames = ilmExplain != null ? Object.keys(ilmExplain) : []; - - return indexNames.reduce( - (acc, indexName) => ({ - hot: - acc.hot + - getPhaseCount({ - ilmExplain, - ilmPhase: 'hot', - indexName, - }), - warm: - acc.warm + - getPhaseCount({ - ilmExplain, - ilmPhase: 'warm', - indexName, - }), - cold: - acc.cold + - getPhaseCount({ - ilmExplain, - ilmPhase: 'cold', - indexName, - }), - frozen: - acc.frozen + - getPhaseCount({ - ilmExplain, - ilmPhase: 'frozen', - indexName, - }), - unmanaged: - acc.unmanaged + - getPhaseCount({ - ilmExplain, - ilmPhase: 'unmanaged', - indexName, - }), - }), - { - hot: 0, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 0, - } - ); -}; - -export const getIndexIncompatible = ({ - indexName, - results, -}: { - indexName: string; - results: Record | undefined; -}): number | undefined => { - if (results == null || results[indexName] == null) { - return undefined; - } - - return results[indexName].incompatible; -}; - -export const getSummaryTableItems = ({ - ilmExplain, - indexNames, - isILMAvailable, - pattern, - patternDocsCount, - results, - sortByColumn, - sortByDirection, - stats, -}: { - ilmExplain: Record | null; - indexNames: string[]; - isILMAvailable: boolean; - pattern: string; - patternDocsCount: number; - results: Record | undefined; - sortByColumn: string; - sortByDirection: 'desc' | 'asc'; - stats: Record | null; -}): IndexSummaryTableItem[] => { - const summaryTableItems = indexNames.map((indexName) => ({ - docsCount: getDocsCount({ stats, indexName }), - incompatible: getIndexIncompatible({ indexName, results }), - indexName, - ilmPhase: - isILMAvailable && ilmExplain != null - ? getIlmPhase(ilmExplain[indexName], isILMAvailable) - : undefined, - pattern, - patternDocsCount, - sizeInBytes: getSizeInBytes({ stats, indexName }), - checkedAt: results?.[indexName]?.checkedAt, - })); - - return orderBy([sortByColumn], [sortByDirection], summaryTableItems); -}; - -export const shouldCreateIndexNames = ({ - ilmExplain, - indexNames, - isILMAvailable, - newIndexNames, - stats, -}: { - ilmExplain: Record | null; - indexNames: string[] | undefined; - isILMAvailable: boolean; - newIndexNames: string[]; - stats: Record | null; -}): boolean => { - return ( - !isEqual(newIndexNames, indexNames) && - stats != null && - ((isILMAvailable && ilmExplain != null) || !isILMAvailable) - ); -}; - -export const shouldCreatePatternRollup = ({ - error, - ilmExplain, - isILMAvailable, - newDocsCount, - patternRollup, - stats, -}: { - error: string | null; - ilmExplain: Record | null; - isILMAvailable: boolean; - newDocsCount: number; - patternRollup: PatternRollup | undefined; - stats: Record | null; -}): boolean => { - if (patternRollup?.docsCount === newDocsCount) { - return false; - } - - const allDataLoaded: boolean = - stats != null && ((isILMAvailable && ilmExplain != null) || !isILMAvailable); - const errorOccurred: boolean = error != null; - - return allDataLoaded || errorOccurred; -}; - -export const getIndexPropertiesContainerId = ({ - indexName, - pattern, -}: { - indexName: string; - pattern: string; -}): string => `index-properties-container-${pattern}${indexName}`; - -export const defaultSort: SortConfig = { - sort: { - direction: 'desc', - field: 'docsCount', - }, -}; - -export const MIN_PAGE_SIZE = 10; - -export const getPageIndex = ({ - indexName, - items, - pageSize, -}: { - indexName: string; - items: IndexSummaryTableItem[]; - pageSize: number; -}): number | null => { - const index = items.findIndex((x) => x.indexName === indexName); - - if (index !== -1 && pageSize !== 0) { - return Math.floor(index / pageSize); - } else { - return null; - } -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx index a59fe7f87d3a5..fc3c698c7a460 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx @@ -9,22 +9,13 @@ import { EuiSpacer, useGeneratedHtmlId } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ErrorEmptyPrompt } from './error_empty_prompt'; -import { - defaultSort, - getIlmExplainPhaseCounts, - getPageIndex, - getSummaryTableItems, - MIN_PAGE_SIZE, - shouldCreateIndexNames, - shouldCreatePatternRollup, -} from './helpers'; import { getTotalPatternIncompatible, getTotalPatternIndicesChecked } from '../../../utils/stats'; import { getIndexNames, getPatternDocsCount, getPatternSizeInBytes } from './utils/stats'; import { LoadingEmptyPrompt } from './loading_empty_prompt'; import { PatternSummary } from './pattern_summary'; import { RemoteClustersCallout } from './remote_clusters_callout'; import { SummaryTable } from './summary_table'; -import { getSummaryTableColumns } from './summary_table/helpers'; +import { getSummaryTableColumns } from './summary_table/utils/columns'; import * as i18n from './translations'; import type { PatternRollup, SelectedIndex, SortConfig } from '../../../types'; import { useIlmExplain } from './hooks/use_ilm_explain'; @@ -34,6 +25,13 @@ 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'; +import { getSummaryTableItems } from '../../../utils/get_summary_table_items'; +import { defaultSort } from '../../../constants'; +import { MIN_PAGE_SIZE } from './constants'; +import { getIlmExplainPhaseCounts } from './utils/ilm_explain'; +import { shouldCreateIndexNames } from './utils/should_create_index_names'; +import { shouldCreatePatternRollup } from './utils/should_create_pattern_rollup'; +import { getPageIndex } from './utils/get_page_index'; const EMPTY_INDEX_NAMES: string[] = []; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx index 6748fd0651799..0ae749a216856 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx @@ -21,6 +21,8 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect } from 'react'; import moment from 'moment'; + +import { getIlmPhase } from '../../../../utils/get_ilm_phase'; import { getDocsCount, getSizeInBytes } from '../../../../utils/stats'; import { useIndicesCheckContext } from '../../../../contexts/indices_check_context'; @@ -28,7 +30,6 @@ import { EMPTY_STAT } from '../../../../constants'; 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 './hooks/use_current_window_width'; import { CHECK_NOW } from './translations'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.test.ts deleted file mode 100644 index 962fa7a825714..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.test.ts +++ /dev/null @@ -1,249 +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 { getMappingsProperties, getSortedPartitionedFieldMetadata } from './helpers'; -import { mockIndicesGetMappingIndexMappingRecords } from '../../../../../mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record'; -import { mockMappingsProperties } from '../../../../../mock/mappings_properties/mock_mappings_properties'; -import { EcsFlatTyped } from '../../../../../constants'; - -describe('helpers', () => { - describe('getSortedPartitionedFieldMetadata', () => { - test('it returns null when mappings are loading', () => { - expect( - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings: true, // <-- - mappingsProperties: mockMappingsProperties, - unallowedValues: {}, - }) - ).toBeNull(); - }); - - test('it returns null when `unallowedValues` is null', () => { - expect( - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings: false, - mappingsProperties: mockMappingsProperties, - unallowedValues: null, // <-- - }) - ).toBeNull(); - }); - - describe('when `mappingsProperties` is unknown', () => { - const incompatibleFieldMetadata = { - ...EcsFlatTyped['@timestamp'], - hasEcsMetadata: true, - indexFieldName: '@timestamp', - indexFieldType: '-', - indexInvalidValues: [], - isEcsCompliant: false, - isInSameFamily: false, - }; - const expected = { - all: [incompatibleFieldMetadata], - custom: [], - ecsCompliant: [], - incompatible: [incompatibleFieldMetadata], - sameFamily: [], - }; - - test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is undefined', () => { - expect( - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings: false, - mappingsProperties: undefined, // <-- - unallowedValues: {}, - }) - ).toEqual(expected); - }); - - test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is null', () => { - expect( - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings: false, - mappingsProperties: null, // <-- - unallowedValues: {}, - }) - ).toEqual(expected); - }); - }); - - test('it returns the expected sorted field metadata', () => { - const unallowedValues = { - 'event.category': [ - { - count: 2, - fieldName: 'an_invalid_category', - }, - { - count: 1, - fieldName: 'theory', - }, - ], - 'event.kind': [], - 'event.outcome': [], - 'event.type': [], - }; - - expect( - getSortedPartitionedFieldMetadata({ - ecsMetadata: EcsFlatTyped, - loadingMappings: false, - mappingsProperties: mockMappingsProperties, - unallowedValues, - }) - ).toMatchObject({ - all: expect.arrayContaining([ - expect.objectContaining({ - name: expect.any(String), - flat_name: expect.any(String), - dashed_name: expect.any(String), - description: expect.any(String), - hasEcsMetadata: true, - isEcsCompliant: expect.any(Boolean), - isInSameFamily: expect.any(Boolean), - }), - ]), - ecsCompliant: expect.arrayContaining([ - expect.objectContaining({ - name: expect.any(String), - flat_name: expect.any(String), - dashed_name: expect.any(String), - description: expect.any(String), - hasEcsMetadata: true, - isEcsCompliant: true, - isInSameFamily: false, - }), - ]), - custom: expect.arrayContaining([ - expect.objectContaining({ - indexFieldName: expect.any(String), - indexFieldType: expect.any(String), - indexInvalidValues: expect.any(Array), - hasEcsMetadata: expect.any(Boolean), - isEcsCompliant: expect.any(Boolean), - isInSameFamily: expect.any(Boolean), - }), - ]), - incompatible: expect.arrayContaining([ - expect.objectContaining({ - name: expect.any(String), - flat_name: expect.any(String), - dashed_name: expect.any(String), - description: expect.any(String), - hasEcsMetadata: expect.any(Boolean), - isEcsCompliant: false, - isInSameFamily: false, - }), - ]), - sameFamily: [], - }); - }); - }); - - describe('getMappingsProperties', () => { - test('it returns the expected mapping properties', () => { - expect( - getMappingsProperties({ - indexes: mockIndicesGetMappingIndexMappingRecords, - indexName: 'auditbeat-custom-index-1', - }) - ).toEqual({ - '@timestamp': { - type: 'date', - }, - event: { - properties: { - category: { - ignore_above: 1024, - type: 'keyword', - }, - }, - }, - host: { - properties: { - name: { - fields: { - keyword: { - ignore_above: 256, - type: 'keyword', - }, - }, - type: 'text', - }, - }, - }, - some: { - properties: { - field: { - fields: { - keyword: { - ignore_above: 256, - type: 'keyword', - }, - }, - type: 'text', - }, - }, - }, - source: { - properties: { - ip: { - fields: { - keyword: { - ignore_above: 256, - type: 'keyword', - }, - }, - type: 'text', - }, - port: { - type: 'long', - }, - }, - }, - }); - }); - - test('it returns null when `indexes` is null', () => { - expect( - getMappingsProperties({ - indexes: null, // <-- - indexName: 'auditbeat-custom-index-1', - }) - ).toBeNull(); - }); - - test('it returns null when `indexName` does not exist in `indexes`', () => { - expect( - getMappingsProperties({ - indexes: mockIndicesGetMappingIndexMappingRecords, - indexName: 'does-not-exist', // <-- - }) - ).toBeNull(); - }); - - test('it returns null when `properties` does not exist in the mappings', () => { - const missingProperties = { - ...mockIndicesGetMappingIndexMappingRecords, - foozle: { - mappings: {}, // <-- does not have a `properties` - }, - }; - - expect( - getMappingsProperties({ - indexes: missingProperties, - indexName: 'foozle', - }) - ).toBeNull(); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.ts deleted file mode 100644 index cf4ae6562b8ed..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers.ts +++ /dev/null @@ -1,92 +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 { - IndicesGetMappingIndexMappingRecord, - MappingProperty, -} from '@elastic/elasticsearch/lib/api/types'; -import { sortBy } from 'lodash/fp'; - -import { EcsFlatTyped } from '../../../../../constants'; -import type { PartitionedFieldMetadata, UnallowedValueCount } from '../../../../../types'; -import { - getEnrichedFieldMetadata, - getFieldTypes, - getMissingTimestampFieldMetadata, - getPartitionedFieldMetadata, -} from './utils/metadata'; - -export const ALL_TAB_ID = 'allTab'; -export const ECS_COMPLIANT_TAB_ID = 'ecsCompliantTab'; -export const CUSTOM_TAB_ID = 'customTab'; -export const INCOMPATIBLE_TAB_ID = 'incompatibleTab'; -export const SAME_FAMILY_TAB_ID = 'sameFamilyTab'; - -export const EMPTY_METADATA: PartitionedFieldMetadata = { - all: [], - ecsCompliant: [], - custom: [], - incompatible: [], - sameFamily: [], -}; - -export const getSortedPartitionedFieldMetadata = ({ - ecsMetadata, - loadingMappings, - mappingsProperties, - unallowedValues, -}: { - ecsMetadata: EcsFlatTyped; - loadingMappings: boolean; - mappingsProperties: Record | null | undefined; - unallowedValues: Record | null; -}): PartitionedFieldMetadata | null => { - if (loadingMappings || unallowedValues == null) { - return null; - } - - // this covers scenario when we try to check an empty index - // or index without required @timestamp field in the mapping - // - // we create an artifical incompatible timestamp field metadata - // so that we can signal to user that the incompatibility is due to missing timestamp - if (mappingsProperties == null) { - const missingTimestampFieldMetadata = getMissingTimestampFieldMetadata(); - return { - ...EMPTY_METADATA, - all: [missingTimestampFieldMetadata], - incompatible: [missingTimestampFieldMetadata], - }; - } - - const fieldTypes = getFieldTypes(mappingsProperties); - - const enrichedFieldMetadata = sortBy( - 'indexFieldName', - fieldTypes.map((fieldMetadata) => - getEnrichedFieldMetadata({ ecsMetadata, fieldMetadata, unallowedValues }) - ) - ); - - const partitionedFieldMetadata = getPartitionedFieldMetadata(enrichedFieldMetadata); - - return partitionedFieldMetadata; -}; - -export const getMappingsProperties = ({ - indexes, - indexName, -}: { - indexes: Record | null; - indexName: string; -}): Record | null => { - if (indexes != null && indexes[indexName] != null) { - return indexes[indexName].mappings.properties ?? null; - } - - return null; -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx index f28d506cda0fa..03d293a02e69a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index.tsx @@ -10,13 +10,13 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { ErrorEmptyPrompt } from '../../error_empty_prompt'; import { LoadingEmptyPrompt } from '../../loading_empty_prompt'; -import { getIndexPropertiesContainerId } from '../../helpers'; import * as i18n from './translations'; import type { IlmPhase, PatternRollup } from '../../../../../types'; import { useIndicesCheckContext } from '../../../../../contexts/indices_check_context'; import { IndexCheckFields } from './index_check_fields'; import { IndexStatsPanel } from './index_stats_panel'; import { useDataQualityContext } from '../../../../../data_quality_context'; +import { getIndexPropertiesContainerId } from './utils/get_index_properties_container_id'; export interface Props { docsCount: number; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts new file mode 100644 index 0000000000000..889f014a66f1b --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ALL_TAB_ID = 'allTab'; +export const ECS_COMPLIANT_TAB_ID = 'ecsCompliantTab'; +export const CUSTOM_TAB_ID = 'customTab'; +export const INCOMPATIBLE_TAB_ID = 'incompatibleTab'; +export const SAME_FAMILY_TAB_ID = 'sameFamilyTab'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx index db6696e36212a..7e69a906c42ad 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/index.tsx @@ -9,9 +9,10 @@ import React, { useMemo, useState } from 'react'; import { EuiButtonGroup, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; +import { EMPTY_METADATA } from '../../../../../../constants'; import { useDataQualityContext } from '../../../../../../data_quality_context'; import { useIndicesCheckContext } from '../../../../../../contexts/indices_check_context'; -import { EMPTY_METADATA, INCOMPATIBLE_TAB_ID } from '../helpers'; +import { INCOMPATIBLE_TAB_ID } from './constants'; import { IlmPhase, PatternRollup } from '../../../../../../types'; import { getTabs } from './tabs/helpers'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx index fa4e63afc6f3b..149697dc3a741 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/helpers.tsx @@ -21,7 +21,7 @@ import { ECS_COMPLIANT_TAB_ID, INCOMPATIBLE_TAB_ID, SAME_FAMILY_TAB_ID, -} from '../../helpers'; +} from '../constants'; import { getMarkdownComment } from '../../markdown/helpers'; import * as i18n from '../../translations'; import { SameFamilyTab } from './same_family_tab'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers.ts index cc32fab713881..ece1636cbad5f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers.ts @@ -41,7 +41,6 @@ import type { PatternRollup, UnallowedValueCount, } from '../../../../../../types'; -import { getDocsCountPercent } from '../../../summary_table/helpers'; import { DOCS, ILM_PHASE, @@ -53,6 +52,7 @@ import { SIZE, } from '../../../summary_table/translations'; import { DATA_QUALITY_TITLE } from '../../../../../../translations'; +import { getDocsCountPercent } from '../../../utils/stats'; export const EMPTY_PLACEHOLDER = '--'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts new file mode 100644 index 0000000000000..68f4414a624a0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getIndexPropertiesContainerId } from './get_index_properties_container_id'; + +describe('getIndexPropertiesContainerId', () => { + const pattern = 'auditbeat-*'; + const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + + test('it returns the expected id', () => { + expect(getIndexPropertiesContainerId({ indexName, pattern })).toEqual( + 'index-properties-container-auditbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001' + ); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts new file mode 100644 index 0000000000000..2a7172d58a7b4 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_index_properties_container_id.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getIndexPropertiesContainerId = ({ + indexName, + pattern, +}: { + indexName: string; + pattern: string; +}): string => `index-properties-container-${pattern}${indexName}`; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx index 9dcd3ca18e729..5128130971b09 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/index.tsx @@ -9,7 +9,8 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { getIndexResultBadgeColor, getIndexResultToolTip } from './helpers'; +import { getIndexResultToolTip } from '../utils/get_index_result_tooltip'; +import { getIndexResultBadgeColor } from './utils/get_index_result_badge_color'; import * as i18n from './translations'; const StyledBadge = styled(EuiBadge)` diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts new file mode 100644 index 0000000000000..68a5da877da9a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.test.ts @@ -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 { getIndexResultBadgeColor } from './get_index_result_badge_color'; + +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'); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts new file mode 100644 index 0000000000000..61f3aaa89a372 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/utils/get_index_result_badge_color.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getIndexResultBadgeColor = (incompatible: number | undefined): string => { + if (incompatible == null) { + return 'ghost'; + } else if (incompatible === 0) { + return '#6dcbb1'; + } else { + return 'danger'; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.test.ts deleted file mode 100644 index c00dac59be3c0..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.test.ts +++ /dev/null @@ -1,97 +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 { getPatternResultTooltip, showResult } from './helpers'; -import { ALL_PASSED, SOME_FAILED, SOME_UNCHECKED } from './translations'; - -describe('helpers', () => { - describe('getPatternResultTooltip', () => { - test('it returns the expected tool tip when `incompatible` is undefined', () => { - expect(getPatternResultTooltip(undefined)).toEqual(SOME_UNCHECKED); - }); - - test('it returns the expected tool tip when `incompatible` is zero', () => { - expect(getPatternResultTooltip(0)).toEqual(ALL_PASSED); - }); - - test('it returns the expected tool tip when `incompatible` is non-zero', () => { - expect(getPatternResultTooltip(1)).toEqual(SOME_FAILED); - }); - }); - - describe('showResult', () => { - test('it returns true when `incompatible` is defined, and `indicesChecked` equals `indices`', () => { - const incompatible = 0; // none of the indices checked had incompatible fields - const indicesChecked = 2; // all indices were checked - const indices = 2; // total indices - - expect( - showResult({ - incompatible, - indices, - indicesChecked, - }) - ).toBe(true); - }); - - test('it returns false when `incompatible` is defined, and `indices` does NOT equal `indicesChecked`', () => { - const incompatible = 0; // the one index checked (so far) didn't have any incompatible fields - const indicesChecked = 1; // only one index has been checked so far - const indices = 2; // total indices - - expect( - showResult({ - incompatible, - indices, - indicesChecked, - }) - ).toBe(false); - }); - - test('it returns false when `incompatible` is undefined', () => { - const incompatible = undefined; // a state of undefined indicates there are no results - const indicesChecked = 1; // all indices were checked - const indices = 1; // total indices - - expect( - showResult({ - incompatible, - indices, - indicesChecked, - }) - ).toBe(false); - }); - - test('it returns false when `indices` is undefined', () => { - const incompatible = 0; // none of the indices checked had incompatible fields - const indicesChecked = 2; // all indices were checked - const indices = undefined; // the total number of indices is unknown - - expect( - showResult({ - incompatible, - indices, - indicesChecked, - }) - ).toBe(false); - }); - - test('it returns false when `indicesChecked` is undefined', () => { - const incompatible = 0; // none of the indices checked had incompatible fields - const indicesChecked = undefined; // no indices were checked - const indices = 2; // total indices - - expect( - showResult({ - incompatible, - indices, - indicesChecked, - }) - ).toBe(false); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/ilm_phase_counts/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/ilm_phase_counts/index.test.tsx index f9e04fc707b01..5de02814fd5cd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/ilm_phase_counts/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/ilm_phase_counts/index.test.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { TestExternalProviders } from '../../../../../../mock/test_providers/test_providers'; import { IlmPhaseCounts } from '.'; -import { getIlmExplainPhaseCounts } from '../../../helpers'; +import { getIlmExplainPhaseCounts } from '../../../utils/ilm_explain'; 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_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx index 03fede1fb7675..f88c9b8f977b0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/index.tsx @@ -8,11 +8,12 @@ import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { getPatternResultTooltip, showResult } from './helpers'; -import { IlmPhaseCounts } from './ilm_phase_counts'; -import * as i18n from '../translations'; import type { IlmExplainPhaseCounts } from '../../../../../types'; import { IndexResultBadge } from '../../index_result_badge'; +import * as i18n from '../translations'; +import { IlmPhaseCounts } from './ilm_phase_counts'; +import { getPatternResultTooltip } from './utils/get_pattern_result_tooltip'; +import { showResult } from './utils/show_result'; interface Props { incompatible: number | undefined; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.test.ts new file mode 100644 index 0000000000000..525eaab098ba6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getPatternResultTooltip } from './get_pattern_result_tooltip'; +import { ALL_PASSED, SOME_FAILED, SOME_UNCHECKED } from '../translations'; + +describe('helpers', () => { + describe('getPatternResultTooltip', () => { + test('it returns the expected tool tip when `incompatible` is undefined', () => { + expect(getPatternResultTooltip(undefined)).toEqual(SOME_UNCHECKED); + }); + + test('it returns the expected tool tip when `incompatible` is zero', () => { + expect(getPatternResultTooltip(0)).toEqual(ALL_PASSED); + }); + + test('it returns the expected tool tip when `incompatible` is non-zero', () => { + expect(getPatternResultTooltip(1)).toEqual(SOME_FAILED); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.ts new file mode 100644 index 0000000000000..2bb51c19ced31 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/get_pattern_result_tooltip.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from '../translations'; + +export const getPatternResultTooltip = (incompatible: number | undefined): string => { + if (incompatible == null) { + return i18n.SOME_UNCHECKED; + } else if (incompatible === 0) { + return i18n.ALL_PASSED; + } else { + return i18n.SOME_FAILED; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.test.ts new file mode 100644 index 0000000000000..4957f8c65c2da --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { showResult } from './show_result'; + +describe('showResult', () => { + test('it returns true when `incompatible` is defined, and `indicesChecked` equals `indices`', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = 2; // all indices were checked + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(true); + }); + + test('it returns false when `incompatible` is defined, and `indices` does NOT equal `indicesChecked`', () => { + const incompatible = 0; // the one index checked (so far) didn't have any incompatible fields + const indicesChecked = 1; // only one index has been checked so far + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `incompatible` is undefined', () => { + const incompatible = undefined; // a state of undefined indicates there are no results + const indicesChecked = 1; // all indices were checked + const indices = 1; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `indices` is undefined', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = 2; // all indices were checked + const indices = undefined; // the total number of indices is unknown + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `indicesChecked` is undefined', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = undefined; // no indices were checked + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.ts similarity index 66% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.ts index bce8098e963fc..df7be212f4bc0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/pattern_summary/pattern_label/utils/show_result.ts @@ -5,17 +5,6 @@ * 2.0. */ -import * as i18n from './translations'; - -export const getPatternResultTooltip = (incompatible: number | undefined): string => { - if (incompatible == null) { - return i18n.SOME_UNCHECKED; - } else if (incompatible === 0) { - return i18n.ALL_PASSED; - } else { - return i18n.SOME_FAILED; - } -}; interface ShowResultProps { incompatible: T; indices: T; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.test.tsx index 01d45a3784a5f..223eaf29dd469 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.test.tsx @@ -10,7 +10,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { EMPTY_STAT } from '../../../../constants'; -import { getSummaryTableColumns } from './helpers'; +import { getSummaryTableColumns } from './utils/columns'; 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'; @@ -18,9 +18,9 @@ import { TestDataQualityProviders, TestExternalProviders, } from '../../../../mock/test_providers/test_providers'; -import { getSummaryTableItems } from '../helpers'; import { SortConfig } from '../../../../types'; import { Props, SummaryTable } from '.'; +import { getSummaryTableItems } from '../../../../utils/get_summary_table_items'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -39,7 +39,7 @@ const indexNames = [ '.ds-packetbeat-8.6.1-2023.02.04-000001', ]; -export const defaultSort: SortConfig = { +const defaultSort: SortConfig = { sort: { direction: 'desc', field: 'docsCount', diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx index fad209fb29e54..bc4e572e892fa 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx @@ -9,12 +9,12 @@ import type { CriteriaWithPagination, EuiBasicTableColumn, Pagination } from '@e import { EuiInMemoryTable } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { getShowPagination } from './helpers'; -import { defaultSort, MIN_PAGE_SIZE } from '../helpers'; -import { SortConfig } from '../../../../types'; +import { defaultSort } from '../../../../constants'; +import { IndexSummaryTableItem, SortConfig } from '../../../../types'; import { useDataQualityContext } from '../../../../data_quality_context'; -import { IndexSummaryTableItem } from '../types'; import { UseIndicesCheckCheckState } from '../../../../hooks/use_indices_check/types'; +import { MIN_PAGE_SIZE } from '../constants'; +import { getShowPagination } from './utils/get_show_pagination'; export interface Props { getTableColumns: ({ diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts index 7e005ba659c1d..88f14c0fa98d6 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/translations.ts @@ -35,13 +35,6 @@ export const EXPAND_ROWS = i18n.translate( } ); -export const FAILED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip', - { - defaultMessage: 'Failed', - } -); - export const ILM_PHASE = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn', { @@ -90,13 +83,6 @@ export const INDEX_TOOL_TIP = (pattern: string) => defaultMessage: 'This index matches the pattern or index name: {pattern}', }); -export const PASSED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.passedTooltip', - { - defaultMessage: 'Passed', - } -); - export const RESULT = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn', { @@ -118,13 +104,6 @@ export const LAST_CHECK = i18n.translate( } ); -export const THIS_INDEX_HAS_NOT_BEEN_CHECKED = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.thisIndexHasNotBeenCheckedTooltip', - { - defaultMessage: 'This index has not been checked', - } -); - export const ACTIONS = i18n.translate( 'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.actionsColumn', { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx similarity index 88% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.test.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx index 1efaa01d36f9e..930b2a6bfbf96 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx @@ -16,20 +16,17 @@ import userEvent from '@testing-library/user-event'; import { omit } from 'lodash/fp'; import React from 'react'; -import { TestExternalProviders } from '../../../../mock/test_providers/test_providers'; -import { EMPTY_STAT } from '../../../../constants'; +import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers'; +import { EMPTY_STAT } from '../../../../../constants'; import { - getDocsCountPercent, getIncompatibleStatColor, - getShowPagination, getSummaryTableColumns, getSummaryTableILMPhaseColumn, getSummaryTableSizeInBytesColumn, - getToggleButtonId, -} from './helpers'; -import { CHECK_INDEX, VIEW_CHECK_DETAILS } from './translations'; -import { IndexSummaryTableItem } from '../types'; -import { getCheckState } from '../../../../stub/get_check_state'; +} from './columns'; +import { CHECK_INDEX, VIEW_CHECK_DETAILS } from '../translations'; +import { IndexSummaryTableItem } from '../../../../../types'; +import { getCheckState } from '../../../../../stub/get_check_state'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -40,59 +37,6 @@ const formatNumber = (value: number | undefined) => value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; describe('helpers', () => { - describe('getDocsCountPercent', () => { - test('it returns an empty string when `patternDocsCount` is zero', () => { - expect( - getDocsCountPercent({ - docsCount: 0, - patternDocsCount: 0, - }) - ).toEqual(''); - }); - - test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is undefined', () => { - expect( - getDocsCountPercent({ - docsCount: 2904, - locales: undefined, - patternDocsCount: 57410, - }) - ).toEqual('5.1%'); - }); - - test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is provided', () => { - expect( - getDocsCountPercent({ - docsCount: 2904, - locales: 'en-US', - patternDocsCount: 57410, - }) - ).toEqual('5.1%'); - }); - }); - - describe('getToggleButtonId', () => { - test('it returns the expected id when the button is expanded', () => { - expect( - getToggleButtonId({ - indexName: 'auditbeat-custom-index-1', - isExpanded: true, - pattern: 'auditbeat-*', - }) - ).toEqual('collapseauditbeat-custom-index-1auditbeat-*'); - }); - - test('it returns the expected id when the button is collapsed', () => { - expect( - getToggleButtonId({ - indexName: 'auditbeat-custom-index-1', - isExpanded: false, - pattern: 'auditbeat-*', - }) - ).toEqual('expandauditbeat-custom-index-1auditbeat-*'); - }); - }); - describe('getSummaryTableColumns', () => { const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; const isILMAvailable = true; @@ -638,35 +582,6 @@ describe('helpers', () => { }); }); - describe('getShowPagination', () => { - test('it returns true when `totalItemCount` is greater than `minPageSize`', () => { - expect( - getShowPagination({ - minPageSize: 10, - totalItemCount: 11, - }) - ).toBe(true); - }); - - test('it returns false when `totalItemCount` equals `minPageSize`', () => { - expect( - getShowPagination({ - minPageSize: 10, - totalItemCount: 10, - }) - ).toBe(false); - }); - - test('it returns false when `totalItemCount` is less than `minPageSize`', () => { - expect( - getShowPagination({ - minPageSize: 10, - totalItemCount: 9, - }) - ).toBe(false); - }); - }); - describe('getIncompatibleStatColor', () => { test('it returns the expected color when incompatible is greater than zero', () => { const incompatible = 123; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx similarity index 80% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.tsx rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx index 327c5b5e46667..09d8187390a9b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/helpers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx @@ -18,48 +18,22 @@ import moment from 'moment'; import styled from 'styled-components'; import { euiThemeVars } from '@kbn/ui-theme'; -import { EMPTY_STAT } from '../../../../constants'; -import { getIlmPhaseDescription } from '../../../../utils/get_ilm_phase_description'; -import { INCOMPATIBLE_INDEX_TOOL_TIP } from '../../../../stat_label/translations'; -import { INDEX_SIZE_TOOLTIP } from '../../../../translations'; -import * as i18n from './translations'; -import { IndexSummaryTableItem } from '../types'; -import { UseIndicesCheckCheckState } from '../../../../hooks/use_indices_check/types'; -import { IndexResultBadge } from '../index_result_badge'; -import { getIndexResultToolTip } from '../index_result_badge/helpers'; -import { Stat } from '../../../../stat'; +import { IndexSummaryTableItem } from '../../../../../types'; +import { EMPTY_STAT } from '../../../../../constants'; +import { getIlmPhaseDescription } from '../../../../../utils/get_ilm_phase_description'; +import { INCOMPATIBLE_INDEX_TOOL_TIP } from '../../../../../stat_label/translations'; +import { INDEX_SIZE_TOOLTIP } from '../../../../../translations'; +import * as i18n from '../translations'; +import { UseIndicesCheckCheckState } from '../../../../../hooks/use_indices_check/types'; +import { IndexResultBadge } from '../../index_result_badge'; +import { Stat } from '../../../../../stat'; +import { getDocsCountPercent } from '../../utils/stats'; +import { getIndexResultToolTip } from '../../utils/get_index_result_tooltip'; const ProgressContainer = styled.div` width: 150px; `; -export const getDocsCountPercent = ({ - docsCount, - locales, - patternDocsCount, -}: { - docsCount: number; - locales?: string | string[]; - patternDocsCount: number; -}): string => - patternDocsCount !== 0 - ? Number(docsCount / patternDocsCount).toLocaleString(locales, { - style: 'percent', - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }) - : ''; - -export const getToggleButtonId = ({ - indexName, - isExpanded, - pattern, -}: { - indexName: string; - isExpanded: boolean; - pattern: string; -}): string => (isExpanded ? `collapse${indexName}${pattern}` : `expand${indexName}${pattern}`); - export const getSummaryTableILMPhaseColumn = ( isILMAvailable: boolean ): Array> => @@ -247,11 +221,3 @@ export const getSummaryTableColumns = ({ width: '120px', }, ]; - -export const getShowPagination = ({ - minPageSize, - totalItemCount, -}: { - minPageSize: number; - totalItemCount: number; -}): boolean => totalItemCount > minPageSize; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.test.ts new file mode 100644 index 0000000000000..7438431e85535 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.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 { getShowPagination } from './get_show_pagination'; + +describe('getShowPagination', () => { + test('it returns true when `totalItemCount` is greater than `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 11, + }) + ).toBe(true); + }); + + test('it returns false when `totalItemCount` equals `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 10, + }) + ).toBe(false); + }); + + test('it returns false when `totalItemCount` is less than `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 9, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.ts new file mode 100644 index 0000000000000..5cf90b3f7a712 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/get_show_pagination.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getShowPagination = ({ + minPageSize, + totalItemCount, +}: { + minPageSize: number; + totalItemCount: number; +}): boolean => totalItemCount > minPageSize; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts index aaf11b1ad405c..8e13a4be4cc30 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/translations.ts @@ -22,3 +22,24 @@ export const LOADING_STATS = i18n.translate( defaultMessage: 'Loading stats', } ); + +export const PASSED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.passedTooltip', + { + defaultMessage: 'Passed', + } +); + +export const FAILED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.failedTooltip', + { + defaultMessage: 'Failed', + } +); + +export const THIS_INDEX_HAS_NOT_BEEN_CHECKED = i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.thisIndexHasNotBeenCheckedTooltip', + { + defaultMessage: 'This index has not been checked', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.test.ts similarity index 58% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.test.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.test.ts index 2f85a6e0ce692..4869aed07941b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.test.ts @@ -5,22 +5,8 @@ * 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'); - }); -}); +import { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../translations'; +import { getIndexResultToolTip } from './get_index_result_tooltip'; describe('getIndexResultToolTip', () => { test('it returns "this index has not been checked" when `incompatible` is undefined', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.ts similarity index 67% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.ts index f9f1c69733059..7b2bb9fb0507e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_result_badge/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_index_result_tooltip.ts @@ -5,17 +5,7 @@ * 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'; - } -}; +import { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../translations'; export const getIndexResultToolTip = (incompatible: number | undefined): string => { if (incompatible == null) { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.test.ts new file mode 100644 index 0000000000000..b2746c830b6de --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.test.ts @@ -0,0 +1,360 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockDataQualityCheckResult } from '../../../../mock/data_quality_check_result/mock_index'; +import { IndexSummaryTableItem } from '../../../../types'; +import { getIndexIncompatible } from '../../../../utils/stats'; +import { getPageIndex } from './get_page_index'; + +describe('helpers', () => { + const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + describe('getIndexIncompatible', () => { + test('it returns undefined when `results` is undefined', () => { + expect( + getIndexIncompatible({ + indexName, + results: undefined, // <-- + }) + ).toBeUndefined(); + }); + + test('it returns undefined when `indexName` is not in the `results`', () => { + expect( + getIndexIncompatible({ + indexName: 'not_in_the_results', // <-- + results: mockDataQualityCheckResult, + }) + ).toBeUndefined(); + }); + + test('it returns the expected count', () => { + expect( + getIndexIncompatible({ + indexName: 'auditbeat-custom-index-1', + results: mockDataQualityCheckResult, + }) + ).toEqual(3); + }); + }); + + describe('getPageIndex', () => { + const getPageIndexArgs: { + indexName: string; + items: IndexSummaryTableItem[]; + pageSize: number; + } = { + indexName: 'auditbeat-7.17.9-2023.04.09-000001', // <-- on page 2 of 3 (page index 1) + items: [ + { + docsCount: 48077, + incompatible: undefined, + indexName: 'auditbeat-7.14.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43357342, + checkedAt: 1706526408000, + }, + { + docsCount: 48068, + incompatible: undefined, + indexName: 'auditbeat-7.3.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 32460397, + checkedAt: 1706526408000, + }, + { + docsCount: 48064, + incompatible: undefined, + indexName: 'auditbeat-7.11.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42782794, + checkedAt: 1706526408000, + }, + { + docsCount: 47868, + incompatible: undefined, + indexName: 'auditbeat-7.6.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 31575964, + checkedAt: 1706526408000, + }, + { + docsCount: 47827, + incompatible: 20, + indexName: 'auditbeat-7.15.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 44130657, + checkedAt: 1706526408000, + }, + { + docsCount: 47642, + incompatible: undefined, + indexName: '.ds-auditbeat-8.4.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42412521, + checkedAt: 1706526408000, + }, + { + docsCount: 47545, + incompatible: undefined, + indexName: 'auditbeat-7.16.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41423244, + checkedAt: 1706526408000, + }, + { + docsCount: 47531, + incompatible: undefined, + indexName: 'auditbeat-7.5.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 32394133, + checkedAt: 1706526408000, + }, + { + docsCount: 47530, + incompatible: undefined, + indexName: 'auditbeat-7.12.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43015519, + checkedAt: 1706526408000, + }, + { + docsCount: 47520, + incompatible: undefined, + indexName: '.ds-auditbeat-8.0.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42230604, + checkedAt: 1706526408000, + }, + { + docsCount: 47496, + incompatible: undefined, + indexName: '.ds-auditbeat-8.2.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41710968, + checkedAt: 1706526408000, + }, + { + docsCount: 47486, + incompatible: undefined, + indexName: '.ds-auditbeat-8.5.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42295944, + checkedAt: 1706526408000, + }, + { + docsCount: 47486, + incompatible: undefined, + indexName: '.ds-auditbeat-8.3.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41761321, + checkedAt: 1706526408000, + }, + { + docsCount: 47460, + incompatible: undefined, + indexName: 'auditbeat-7.2.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 30481198, + checkedAt: 1706526408000, + }, + { + docsCount: 47439, + incompatible: undefined, + indexName: 'auditbeat-7.17.9-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41554041, + checkedAt: 1706526408000, + }, + { + docsCount: 47395, + incompatible: undefined, + indexName: 'auditbeat-7.9.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42815907, + checkedAt: 1706526408000, + }, + { + docsCount: 47394, + incompatible: undefined, + indexName: '.ds-auditbeat-8.7.0-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41157112, + checkedAt: 1706526408000, + }, + { + docsCount: 47372, + incompatible: undefined, + indexName: 'auditbeat-7.4.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 31626792, + checkedAt: 1706526408000, + }, + { + docsCount: 47369, + incompatible: undefined, + indexName: 'auditbeat-7.13.4-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41828969, + checkedAt: 1706526408000, + }, + { + docsCount: 47348, + incompatible: undefined, + indexName: 'auditbeat-7.7.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 40010773, + checkedAt: 1706526408000, + }, + { + docsCount: 47339, + incompatible: undefined, + indexName: 'auditbeat-7.10.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43480570, + checkedAt: 1706526408000, + }, + { + docsCount: 47325, + incompatible: undefined, + indexName: '.ds-auditbeat-8.1.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41822475, + checkedAt: 1706526408000, + }, + { + docsCount: 47294, + incompatible: undefined, + indexName: 'auditbeat-7.8.0-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43018490, + checkedAt: 1706526408000, + }, + { + docsCount: 24276, + incompatible: undefined, + indexName: '.ds-auditbeat-8.6.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 23579440, + checkedAt: 1706526408000, + }, + { + docsCount: 4, + incompatible: undefined, + indexName: 'auditbeat-custom-index-1', + ilmPhase: 'unmanaged', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 28409, + checkedAt: 1706526408000, + }, + { + docsCount: 0, + incompatible: undefined, + indexName: 'auditbeat-custom-empty-index-1', + ilmPhase: 'unmanaged', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 247, + checkedAt: 1706526408000, + }, + ], + pageSize: 10, + }; + + test('it returns the expected page index', () => { + expect(getPageIndex(getPageIndexArgs)).toEqual(1); + }); + + test('it returns the expected page index for the first item', () => { + const firstItemIndexName = 'auditbeat-7.14.2-2023.04.09-000001'; + + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: firstItemIndexName, + }) + ).toEqual(0); + }); + + test('it returns the expected page index for the last item', () => { + const lastItemIndexName = 'auditbeat-custom-empty-index-1'; + + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: lastItemIndexName, + }) + ).toEqual(2); + }); + + test('it returns null when the index cannot be found', () => { + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: 'does_not_exist', // <-- this index is not in the items + }) + ).toBeNull(); + }); + + test('it returns null when `pageSize` is zero', () => { + expect( + getPageIndex({ + ...getPageIndexArgs, + pageSize: 0, // <-- invalid + }) + ).toBeNull(); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.ts new file mode 100644 index 0000000000000..3aff52ca9d3f8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/get_page_index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IndexSummaryTableItem } from '../../../../types'; + +export const getPageIndex = ({ + indexName, + items, + pageSize, +}: { + indexName: string; + items: IndexSummaryTableItem[]; + pageSize: number; +}): number | null => { + const index = items.findIndex((x) => x.indexName === indexName); + + if (index !== -1 && pageSize !== 0) { + return Math.floor(index / pageSize); + } else { + return null; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.test.ts new file mode 100644 index 0000000000000..ad588af866c88 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, + IlmExplainLifecycleLifecycleExplainManaged, + IlmExplainLifecycleLifecycleExplainUnmanaged, +} from '@elastic/elasticsearch/lib/api/types'; +import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; +import { getIlmExplainPhaseCounts, getPhaseCount, isManaged } from './ilm_explain'; + +const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + +const hot: IlmExplainLifecycleLifecycleExplainManaged = { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '3.98d', + lifecycle_date_millis: 1675536751379, + age: '3.98d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, +}; +const warm = { + ...hot, + phase: 'warm', +}; +const cold = { + ...hot, + phase: 'cold', +}; +const frozen = { + ...hot, + phase: 'frozen', +}; + +const managed: Record = { + hot, + warm, + cold, + frozen, +}; + +const unmanaged: IlmExplainLifecycleLifecycleExplainUnmanaged = { + index: 'michael', + managed: false, +}; + +describe('isManaged', () => { + test('it returns true when the `ilmExplainRecord` `managed` property is true', () => { + const ilmExplain = mockIlmExplain[indexName]; + + expect(isManaged(ilmExplain)).toBe(true); + }); + + test('it returns false when the `ilmExplainRecord` is undefined', () => { + expect(isManaged(undefined)).toBe(false); + }); +}); + +describe('getPhaseCount', () => { + test('it returns the expected count when an index with the specified `ilmPhase` exists in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'hot', // this phase is in the record + indexName, // valid index name + }) + ).toEqual(1); + }); + + test('it returns zero when `ilmPhase` is null', () => { + expect( + getPhaseCount({ + ilmExplain: null, + ilmPhase: 'hot', + indexName, + }) + ).toEqual(0); + }); + + test('it returns zero when the `indexName` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'hot', + indexName: 'invalid', // this index does NOT exist + }) + ).toEqual(0); + }); + + test('it returns zero when the specified `ilmPhase` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'warm', // this phase is NOT in the record + indexName, // valid index name + }) + ).toEqual(0); + }); + + describe('when `ilmPhase` is `unmanaged`', () => { + test('it returns the expected count for an `unmanaged` index', () => { + const index = 'auditbeat-custom-index-1'; + const ilmExplainRecord: IlmExplainLifecycleLifecycleExplain = { + index, + managed: false, + }; + const ilmExplain = { + [index]: ilmExplainRecord, + }; + + expect( + getPhaseCount({ + ilmExplain, + ilmPhase: 'unmanaged', // ilmPhase is unmanaged + indexName: index, // an unmanaged index + }) + ).toEqual(1); + }); + + test('it returns zero for a managed index', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'unmanaged', // ilmPhase is unmanaged + indexName, // a managed (`hot`) index + }) + ).toEqual(0); + }); + }); +}); + +describe('getIlmExplainPhaseCounts', () => { + test('it returns the expected counts (all zeros) when `ilmExplain` is null', () => { + expect(getIlmExplainPhaseCounts(null)).toEqual({ + cold: 0, + frozen: 0, + hot: 0, + unmanaged: 0, + warm: 0, + }); + }); + + test('it returns the expected counts', () => { + const ilmExplain: Record = { + ...managed, + [unmanaged.index]: unmanaged, + }; + + expect(getIlmExplainPhaseCounts(ilmExplain)).toEqual({ + cold: 1, + frozen: 1, + hot: 1, + unmanaged: 1, + warm: 1, + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.ts new file mode 100644 index 0000000000000..c47353162b003 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/ilm_explain.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; + +import { IlmExplainPhaseCounts, IlmPhase } from '../../../../types'; + +export const isManaged = ( + ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined +): boolean => ilmExplainRecord?.managed === true; + +export const getPhaseCount = ({ + ilmExplain, + ilmPhase, + indexName, +}: { + ilmExplain: Record | null; + ilmPhase: IlmPhase; + indexName: string; +}): number => { + const ilmExplainRecord = ilmExplain != null ? ilmExplain[indexName] : undefined; + + if (ilmPhase === 'unmanaged') { + return isManaged(ilmExplainRecord) ? 0 : 1; + } else if (ilmExplainRecord != null && 'phase' in ilmExplainRecord) { + return ilmExplainRecord.phase === ilmPhase ? 1 : 0; + } + + return 0; +}; + +export const getIlmExplainPhaseCounts = ( + ilmExplain: Record | null +): IlmExplainPhaseCounts => { + const indexNames = ilmExplain != null ? Object.keys(ilmExplain) : []; + + return indexNames.reduce( + (acc, indexName) => ({ + hot: + acc.hot + + getPhaseCount({ + ilmExplain, + ilmPhase: 'hot', + indexName, + }), + warm: + acc.warm + + getPhaseCount({ + ilmExplain, + ilmPhase: 'warm', + indexName, + }), + cold: + acc.cold + + getPhaseCount({ + ilmExplain, + ilmPhase: 'cold', + indexName, + }), + frozen: + acc.frozen + + getPhaseCount({ + ilmExplain, + ilmPhase: 'frozen', + indexName, + }), + unmanaged: + acc.unmanaged + + getPhaseCount({ + ilmExplain, + ilmPhase: 'unmanaged', + indexName, + }), + }), + { + hot: 0, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + } + ); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.test.ts new file mode 100644 index 0000000000000..7d663b82fc1ff --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; +import { shouldCreateIndexNames } from './should_create_index_names'; +import { mockStats } from '../../../../mock/stats/mock_stats'; + +describe('shouldCreateIndexNames', () => { + const indexNames = [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + 'auditbeat-custom-index-1', + ]; + const isILMAvailable = true; + + test('returns true when `indexNames` does NOT exist, and the required `stats` and `ilmExplain` are available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames: undefined, + isILMAvailable, + newIndexNames: [], + stats: mockStats, + }) + ).toBe(true); + }); + + test('returns true when `isILMAvailable` is false, and the required `stats` is available, and `ilmExplain` is not available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames: undefined, + isILMAvailable: false, + newIndexNames: [], + stats: mockStats, + }) + ).toBe(true); + }); + + test('returns false when `indexNames` exists, and the required `stats` and `ilmExplain` are available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames, + isILMAvailable, + newIndexNames: indexNames, + stats: mockStats, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames: undefined, + isILMAvailable, + newIndexNames: [], + stats: null, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames: undefined, + isILMAvailable, + newIndexNames: [], + stats: mockStats, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames: undefined, + isILMAvailable, + newIndexNames: [], + stats: null, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` exists, `stats` is NOT available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames, + isILMAvailable, + newIndexNames: [], + stats: null, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.ts new file mode 100644 index 0000000000000..25a6038ae9078 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_index_names.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; + +import { isEqual } from 'lodash/fp'; +import { MeteringStatsIndex } from '../../../../types'; + +export const shouldCreateIndexNames = ({ + ilmExplain, + indexNames, + isILMAvailable, + newIndexNames, + stats, +}: { + ilmExplain: Record | null; + indexNames: string[] | undefined; + isILMAvailable: boolean; + newIndexNames: string[]; + stats: Record | null; +}): boolean => { + return ( + !isEqual(newIndexNames, indexNames) && + stats != null && + ((isILMAvailable && ilmExplain != null) || !isILMAvailable) + ); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.test.ts new file mode 100644 index 0000000000000..1adb851d8aafc --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockStats } from '../../../../mock/stats/mock_stats'; +import { shouldCreatePatternRollup } from './should_create_pattern_rollup'; +import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; +import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { getIndexNames, getPatternDocsCount } from './stats'; + +describe('shouldCreatePatternRollup', () => { + const isILMAvailable = true; + const newIndexNames = getIndexNames({ + stats: mockStats, + ilmExplain: mockIlmExplain, + ilmPhases: ['hot', 'unmanaged'], + isILMAvailable, + }); + const newDocsCount = getPatternDocsCount({ indexNames: newIndexNames, stats: mockStats }); + test('it returns false when the `patternRollup.docsCount` equals newDocsCount', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + isILMAvailable, + newDocsCount: auditbeatWithAllResults.docsCount as number, + patternRollup: auditbeatWithAllResults, + stats: mockStats, + }) + ).toBe(false); + }); + + test('it returns true when all data and ILMExplain were loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('it returns true when all data was loaded and ILM is not available', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: null, + isILMAvailable: false, + newDocsCount, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('it returns false when `stats`, but NOT `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: null, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(false); + }); + + test('it returns false when `stats` was NOT loaded, and `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: null, + }) + ).toBe(false); + }); + + test('it returns true if an error occurred, and NO data was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'whoops', + ilmExplain: null, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: null, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and just `stats` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'something went', + ilmExplain: null, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and just `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'horribly wrong', + ilmExplain: mockIlmExplain, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: null, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and all data was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'over here', + ilmExplain: mockIlmExplain, + isILMAvailable, + newDocsCount, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.ts new file mode 100644 index 0000000000000..87773ac01de17 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/should_create_pattern_rollup.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 { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; + +import { MeteringStatsIndex, PatternRollup } from '../../../../types'; + +export const shouldCreatePatternRollup = ({ + error, + ilmExplain, + isILMAvailable, + newDocsCount, + patternRollup, + stats, +}: { + error: string | null; + ilmExplain: Record | null; + isILMAvailable: boolean; + newDocsCount: number; + patternRollup: PatternRollup | undefined; + stats: Record | null; +}): boolean => { + if (patternRollup?.docsCount === newDocsCount) { + return false; + } + + const allDataLoaded: boolean = + stats != null && ((isILMAvailable && ilmExplain != null) || !isILMAvailable); + const errorOccurred: boolean = error != null; + + return allDataLoaded || errorOccurred; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts index 306c9f93d83b6..01e649ebf6bc9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.test.ts @@ -10,7 +10,12 @@ import { mockStatsAuditbeatIndex } from '../../../../mock/stats/mock_stats_packe import { mockStatsPacketbeatIndex } from '../../../../mock/stats/mock_stats_auditbeat_index'; import { mockIlmExplain } from '../../../../mock/ilm_explain/mock_ilm_explain'; import { mockStats } from '../../../../mock/stats/mock_stats'; -import { getIndexNames, getPatternDocsCount, getPatternSizeInBytes } from './stats'; +import { + getDocsCountPercent, + getIndexNames, + getPatternDocsCount, + getPatternSizeInBytes, +} from './stats'; import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; describe('getIndexNames', () => { @@ -232,3 +237,34 @@ describe('getPatternSizeInBytes', () => { ).toEqual(expectedCount); }); }); + +describe('getDocsCountPercent', () => { + test('it returns an empty string when `patternDocsCount` is zero', () => { + expect( + getDocsCountPercent({ + docsCount: 0, + patternDocsCount: 0, + }) + ).toEqual(''); + }); + + test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is undefined', () => { + expect( + getDocsCountPercent({ + docsCount: 2904, + locales: undefined, + patternDocsCount: 57410, + }) + ).toEqual('5.1%'); + }); + + test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is provided', () => { + expect( + getDocsCountPercent({ + docsCount: 2904, + locales: 'en-US', + patternDocsCount: 57410, + }) + ).toEqual('5.1%'); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.ts index 83e2b592dc079..83ece4d3194c3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/utils/stats.ts @@ -7,9 +7,9 @@ import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { getIlmPhase } from '../../../../utils/get_ilm_phase'; import { getDocsCount, getSizeInBytes } from '../../../../utils/stats'; import { MeteringStatsIndex } from '../../../../types'; -import { getIlmPhase } from '../helpers'; export const getPatternDocsCount = ({ indexNames, @@ -70,3 +70,20 @@ export const getIndexNames = ({ return EMPTY_INDEX_NAMES; } }; + +export const getDocsCountPercent = ({ + docsCount, + locales, + patternDocsCount, +}: { + docsCount: number; + locales?: string | string[]; + patternDocsCount: number; +}): string => + patternDocsCount !== 0 + ? Number(docsCount / patternDocsCount).toLocaleString(locales, { + style: 'percent', + maximumFractionDigits: 1, + minimumFractionDigits: 1, + }) + : ''; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.test.ts deleted file mode 100644 index da46cd2b40f33..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.test.ts +++ /dev/null @@ -1,516 +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 { euiThemeVars } from '@kbn/ui-theme'; - -import { EMPTY_STAT } from '../../constants'; -import { - DEFAULT_INDEX_COLOR, - getFillColor, - getFlattenedBuckets, - getGroupFromPath, - getLayersMultiDimensional, - getLegendItems, - getLegendItemsForPattern, - getPathToFlattenedBucketMap, - getPatternLegendItem, - getPatternSizeInBytes, -} 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 { PatternRollup } from '../../types'; - -const defaultBytesFormat = '0,0.[0]b'; -const formatBytes = (value: number | undefined) => - value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; - -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, -}; - -/** a valid `PatternRollup` that has an undefined `sizeInBytes` */ -const noSizeInBytes: Record = { - 'valid-*': { - docsCount: 19127, - error: null, - ilmExplain: null, - ilmExplainPhaseCounts: { - hot: 1, - warm: 0, - cold: 0, - frozen: 0, - unmanaged: 2, - }, - indices: 3, - pattern: 'valid-*', - results: undefined, - sizeInBytes: undefined, // <-- - stats: null, - }, -}; - -describe('helpers', () => { - describe('getPatternSizeInBytes', () => { - test('it returns the expected size when the pattern exists in the rollup', () => { - const pattern = 'auditbeat-*'; - - expect(getPatternSizeInBytes({ pattern, patternRollups })).toEqual( - auditbeatWithAllResults.sizeInBytes - ); - }); - - test('it returns undefined when the pattern exists in the rollup, but does not have a sizeInBytes', () => { - const pattern = 'valid-*'; - - expect(getPatternSizeInBytes({ pattern, patternRollups: noSizeInBytes })).toBeUndefined(); - }); - - test('it returns undefined when the pattern does NOT exist in the rollup', () => { - const pattern = 'does-not-exist-*'; - - expect(getPatternSizeInBytes({ pattern, patternRollups })).toBeUndefined(); - }); - }); - - describe('getPatternLegendItem', () => { - test('it returns the expected legend item', () => { - const pattern = 'auditbeat-*'; - - expect(getPatternLegendItem({ pattern, patternRollups })).toEqual({ - color: null, - ilmPhase: null, - index: null, - pattern, - sizeInBytes: auditbeatWithAllResults.sizeInBytes, - docsCount: auditbeatWithAllResults.docsCount, - }); - }); - }); - - describe('getLegendItemsForPattern', () => { - test('it returns the expected legend items', () => { - const pattern = 'auditbeat-*'; - const flattenedBuckets = getFlattenedBuckets({ - ilmPhases, - isILMAvailable: true, - patternRollups, - }); - - expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([ - { - color: euiThemeVars.euiColorSuccess, - ilmPhase: 'hot', - index: '.ds-auditbeat-8.6.1-2023.02.07-000001', - pattern: 'auditbeat-*', - sizeInBytes: 18791790, - docsCount: 19123, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: 'unmanaged', - index: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 28409, - docsCount: 4, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: 'unmanaged', - index: 'auditbeat-custom-empty-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 247, - docsCount: 0, - }, - ]); - }); - - test('it returns the expected legend items when isILMAvailable is false', () => { - const pattern = 'auditbeat-*'; - const flattenedBuckets = getFlattenedBuckets({ - ilmPhases, - isILMAvailable: false, - patternRollups, - }); - expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([ - { - color: euiThemeVars.euiColorSuccess, - ilmPhase: null, - index: '.ds-auditbeat-8.6.1-2023.02.07-000001', - pattern: 'auditbeat-*', - sizeInBytes: 18791790, - docsCount: 19123, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: null, - index: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 28409, - docsCount: 4, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: null, - index: 'auditbeat-custom-empty-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 247, - docsCount: 0, - }, - ]); - }); - }); - - describe('getLegendItems', () => { - test('it returns the expected legend items', () => { - const flattenedBuckets = getFlattenedBuckets({ - ilmPhases, - isILMAvailable: true, - patternRollups, - }); - - expect(getLegendItems({ flattenedBuckets, patterns, patternRollups })).toEqual([ - { - color: null, - ilmPhase: null, - index: null, - pattern: '.alerts-security.alerts-default', - sizeInBytes: 29717961631, - docsCount: 26093, - }, - { - color: euiThemeVars.euiColorSuccess, - ilmPhase: 'hot', - index: '.internal.alerts-security.alerts-default-000001', - pattern: '.alerts-security.alerts-default', - sizeInBytes: 0, - docsCount: 26093, - }, - { - color: null, - ilmPhase: null, - index: null, - pattern: 'auditbeat-*', - sizeInBytes: 18820446, - docsCount: 19127, - }, - { - color: euiThemeVars.euiColorSuccess, - ilmPhase: 'hot', - index: '.ds-auditbeat-8.6.1-2023.02.07-000001', - pattern: 'auditbeat-*', - sizeInBytes: 18791790, - docsCount: 19123, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: 'unmanaged', - index: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 28409, - docsCount: 4, - }, - { - color: euiThemeVars.euiColorDanger, - ilmPhase: 'unmanaged', - index: 'auditbeat-custom-empty-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 247, - docsCount: 0, - }, - { - color: null, - ilmPhase: null, - index: null, - pattern: 'packetbeat-*', - sizeInBytes: 1096520898, - docsCount: 3258632, - }, - { - color: euiThemeVars.euiColorPrimary, - ilmPhase: 'hot', - index: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 584326147, - docsCount: 1630289, - }, - { - color: euiThemeVars.euiColorPrimary, - ilmPhase: 'hot', - index: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 512194751, - docsCount: 1628343, - }, - ]); - }); - }); - - describe('getFlattenedBuckets', () => { - test('it returns the expected flattened buckets', () => { - expect( - getFlattenedBuckets({ - ilmPhases, - isILMAvailable: true, - patternRollups, - }) - ).toEqual([ - { - ilmPhase: 'hot', - incompatible: 0, - indexName: '.internal.alerts-security.alerts-default-000001', - pattern: '.alerts-security.alerts-default', - sizeInBytes: 0, - docsCount: 26093, - }, - { - ilmPhase: 'hot', - incompatible: 0, - indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', - pattern: 'auditbeat-*', - sizeInBytes: 18791790, - docsCount: 19123, - }, - { - ilmPhase: 'unmanaged', - incompatible: 1, - indexName: 'auditbeat-custom-empty-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 247, - docsCount: 0, - }, - { - ilmPhase: 'unmanaged', - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 28409, - docsCount: 4, - }, - { - ilmPhase: 'hot', - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 512194751, - docsCount: 1628343, - }, - { - ilmPhase: 'hot', - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 584326147, - docsCount: 1630289, - }, - ]); - }); - - test('it returns the expected flattened buckets when isILMAvailable is false', () => { - expect( - getFlattenedBuckets({ - ilmPhases, - isILMAvailable: false, - patternRollups, - }) - ).toEqual([ - { - docsCount: 26093, - ilmPhase: undefined, - incompatible: 0, - indexName: '.internal.alerts-security.alerts-default-000001', - pattern: '.alerts-security.alerts-default', - sizeInBytes: 0, - }, - { - docsCount: 19123, - ilmPhase: undefined, - incompatible: 0, - indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', - pattern: 'auditbeat-*', - sizeInBytes: 18791790, - }, - { - docsCount: 0, - ilmPhase: undefined, - incompatible: 1, - indexName: 'auditbeat-custom-empty-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 247, - }, - { - docsCount: 4, - ilmPhase: undefined, - incompatible: 3, - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - sizeInBytes: 28409, - }, - { - docsCount: 1628343, - ilmPhase: undefined, - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 512194751, - }, - { - docsCount: 1630289, - ilmPhase: undefined, - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'packetbeat-*', - sizeInBytes: 584326147, - }, - ]); - }); - }); - - describe('getFillColor', () => { - test('it returns success when `incompatible` is zero', () => { - const incompatible = 0; - - expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorSuccess); - }); - - test('it returns danger when `incompatible` is greater than 0', () => { - const incompatible = 1; - - expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorDanger); - }); - - test('it returns the default color when `incompatible` is undefined', () => { - const incompatible = undefined; - - expect(getFillColor(incompatible)).toEqual(DEFAULT_INDEX_COLOR); - }); - }); - - describe('getPathToFlattenedBucketMap', () => { - test('it returns the expected map', () => { - const flattenedBuckets = getFlattenedBuckets({ - ilmPhases, - isILMAvailable: true, - patternRollups, - }); - - expect(getPathToFlattenedBucketMap(flattenedBuckets)).toEqual({ - '.alerts-security.alerts-default.internal.alerts-security.alerts-default-000001': { - pattern: '.alerts-security.alerts-default', - indexName: '.internal.alerts-security.alerts-default-000001', - ilmPhase: 'hot', - incompatible: 0, - sizeInBytes: 0, - docsCount: 26093, - }, - 'auditbeat-*.ds-auditbeat-8.6.1-2023.02.07-000001': { - pattern: 'auditbeat-*', - indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', - ilmPhase: 'hot', - incompatible: 0, - sizeInBytes: 18791790, - docsCount: 19123, - }, - 'auditbeat-*auditbeat-custom-empty-index-1': { - pattern: 'auditbeat-*', - indexName: 'auditbeat-custom-empty-index-1', - ilmPhase: 'unmanaged', - incompatible: 1, - sizeInBytes: 247, - docsCount: 0, - }, - 'auditbeat-*auditbeat-custom-index-1': { - pattern: 'auditbeat-*', - indexName: 'auditbeat-custom-index-1', - ilmPhase: 'unmanaged', - incompatible: 3, - sizeInBytes: 28409, - docsCount: 4, - }, - 'packetbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001': { - pattern: 'packetbeat-*', - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - ilmPhase: 'hot', - sizeInBytes: 512194751, - docsCount: 1628343, - }, - 'packetbeat-*.ds-packetbeat-8.5.3-2023.02.04-000001': { - docsCount: 1630289, - pattern: 'packetbeat-*', - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - ilmPhase: 'hot', - sizeInBytes: 584326147, - }, - }); - }); - }); - - describe('getGroupFromPath', () => { - it('returns the expected group from the path', () => { - expect( - getGroupFromPath([ - { - index: 0, - value: '__null_small_multiples_key__', - }, - { - index: 0, - value: '__root_key__', - }, - { - index: 0, - value: 'auditbeat-*', - }, - { - index: 1, - value: 'auditbeat-custom-empty-index-1', - }, - ]) - ).toEqual('auditbeat-*'); - }); - - it('returns undefined when path is an empty array', () => { - expect(getGroupFromPath([])).toBeUndefined(); - }); - - it('returns undefined when path is an array with only one value', () => { - expect( - getGroupFromPath([{ index: 0, value: '__null_small_multiples_key__' }]) - ).toBeUndefined(); - }); - }); - - describe('getLayersMultiDimensional', () => { - const layer0FillColor = 'transparent'; - const flattenedBuckets = getFlattenedBuckets({ - ilmPhases, - isILMAvailable: true, - patternRollups, - }); - const pathToFlattenedBucketMap = getPathToFlattenedBucketMap(flattenedBuckets); - - it('returns the expected number of layers', () => { - expect( - getLayersMultiDimensional({ - valueFormatter: formatBytes, - layer0FillColor, - pathToFlattenedBucketMap, - }).length - ).toEqual(2); - }); - - it('returns the expected fillLabel valueFormatter function', () => { - getLayersMultiDimensional({ - valueFormatter: formatBytes, - layer0FillColor, - pathToFlattenedBucketMap, - }).forEach((x) => expect(x.fillLabel.valueFormatter(123)).toEqual('123B')); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.ts deleted file mode 100644 index 283854a62acf2..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/helpers.ts +++ /dev/null @@ -1,241 +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, Key, ArrayNode } from '@elastic/charts'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { orderBy } from 'lodash/fp'; - -import { getIlmPhase } from '../indices_details/pattern/helpers'; -import { PatternRollup } from '../../types'; -import { getDocsCount, getSizeInBytes } from '../../utils/stats'; - -export interface LegendItem { - color: string | null; - ilmPhase: string | null; - index: string | null; - pattern: string; - sizeInBytes: number | undefined; - docsCount: number; -} - -export interface FlattenedBucket { - ilmPhase: string | undefined; - incompatible: number | undefined; - indexName: string | undefined; - pattern: string; - sizeInBytes: number | undefined; - docsCount: number; -} - -export const getPatternSizeInBytes = ({ - pattern, - patternRollups, -}: { - pattern: string; - patternRollups: Record; -}): number | undefined => { - if (patternRollups[pattern] != null) { - return patternRollups[pattern].sizeInBytes; - } else { - return undefined; - } -}; - -export const getPatternDocsCount = ({ - pattern, - patternRollups, -}: { - pattern: string; - patternRollups: Record; -}): number => { - if (patternRollups[pattern] != null) { - return patternRollups[pattern].docsCount ?? 0; - } else { - return 0; - } -}; - -export const getPatternLegendItem = ({ - pattern, - patternRollups, -}: { - pattern: string; - patternRollups: Record; -}): LegendItem => ({ - color: null, - ilmPhase: null, - index: null, - pattern, - sizeInBytes: getPatternSizeInBytes({ pattern, patternRollups }), - docsCount: getPatternDocsCount({ pattern, patternRollups }), -}); - -export const getLegendItemsForPattern = ({ - pattern, - flattenedBuckets, -}: { - pattern: string; - flattenedBuckets: FlattenedBucket[]; -}): LegendItem[] => - orderBy( - ['sizeInBytes'], - ['desc'], - flattenedBuckets - .filter((x) => x.pattern === pattern) - .map((flattenedBucket) => ({ - color: getFillColor(flattenedBucket.incompatible), - ilmPhase: flattenedBucket.ilmPhase ?? null, - index: flattenedBucket.indexName ?? null, - pattern: flattenedBucket.pattern, - sizeInBytes: flattenedBucket.sizeInBytes, - docsCount: flattenedBucket.docsCount, - })) - ); - -export const getLegendItems = ({ - patterns, - flattenedBuckets, - patternRollups, -}: { - patterns: string[]; - flattenedBuckets: FlattenedBucket[]; - patternRollups: Record; -}): LegendItem[] => - patterns.reduce( - (acc, pattern) => [ - ...acc, - getPatternLegendItem({ pattern, patternRollups }), - ...getLegendItemsForPattern({ pattern, flattenedBuckets }), - ], - [] - ); - -export const getFlattenedBuckets = ({ - ilmPhases, - isILMAvailable, - patternRollups, -}: { - ilmPhases: string[]; - isILMAvailable: boolean; - patternRollups: Record; -}): FlattenedBucket[] => - Object.values(patternRollups).reduce((acc, patternRollup) => { - // enables fast lookup of valid phase names: - const ilmPhasesMap = ilmPhases.reduce>( - (phasesMap, phase) => ({ ...phasesMap, [phase]: 0 }), - {} - ); - const { ilmExplain, pattern, results, stats } = patternRollup; - - if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) { - return [ - ...acc, - ...Object.entries(stats).reduce((validStats, [indexName]) => { - const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable); - const isSelectedPhase = - (isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) || - !isILMAvailable; - - if (isSelectedPhase) { - const incompatible = - results != null && results[indexName] != null - ? results[indexName].incompatible - : undefined; - const sizeInBytes = getSizeInBytes({ indexName, stats }); - const docsCount = getDocsCount({ stats, indexName }); - return [ - ...validStats, - { - ilmPhase, - incompatible, - indexName, - pattern, - sizeInBytes, - docsCount, - }, - ]; - } else { - return validStats; - } - }, []), - ]; - } - - return acc; - }, []); - -const groupByRollup = (d: Datum) => d.pattern; // the treemap is grouped by this field - -export const DEFAULT_INDEX_COLOR = euiThemeVars.euiColorPrimary; - -export const getFillColor = (incompatible: number | undefined): string => { - if (incompatible === 0) { - return euiThemeVars.euiColorSuccess; - } else if (incompatible != null && incompatible > 0) { - return euiThemeVars.euiColorDanger; - } else { - return DEFAULT_INDEX_COLOR; - } -}; - -export const getPathToFlattenedBucketMap = ( - flattenedBuckets: FlattenedBucket[] -): Record => - flattenedBuckets.reduce>( - (acc, { pattern, indexName, ...remaining }) => ({ - ...acc, - [`${pattern}${indexName}`]: { pattern, indexName, ...remaining }, - }), - {} - ); - -/** - * Extracts the first group name from the data representing the second group - */ -export const getGroupFromPath = (path: ArrayNode['path']): string | undefined => { - const OFFSET_FROM_END = 2; // The offset from the end of the path array containing the group - const groupIndex = path.length - OFFSET_FROM_END; - return groupIndex > 0 ? path[groupIndex].value : undefined; -}; - -export const getLayersMultiDimensional = ({ - valueFormatter, - layer0FillColor, - pathToFlattenedBucketMap, -}: { - valueFormatter: (value: number) => string; - layer0FillColor: string; - pathToFlattenedBucketMap: Record; -}) => { - return [ - { - fillLabel: { - valueFormatter, - }, - groupByRollup, - nodeLabel: (ilmPhase: Datum) => ilmPhase, - shape: { - fillColor: layer0FillColor, - }, - }, - { - fillLabel: { - valueFormatter, - }, - groupByRollup: (d: Datum) => d.indexName, - nodeLabel: (indexName: Datum) => indexName, - shape: { - fillColor: (indexName: Key, _sortIndex: number, node: Pick) => { - const pattern = getGroupFromPath(node.path) ?? ''; - const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`]; - - return getFillColor(flattenedBucket?.incompatible); - }, - }, - }, - ]; -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx index cfde7c0342585..23a4fc46f6ad4 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/index.tsx @@ -8,12 +8,12 @@ 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 } from '../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { SelectedIndex } from '../../types'; import { useDataQualityContext } from '../../data_quality_context'; import { DOCS_UNIT } from './translations'; +import { getFlattenedBuckets } from './utils/get_flattened_buckets'; export interface Props { onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/constants.ts new file mode 100644 index 0000000000000..c67b052fbf2b5 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; + +export const DEFAULT_INDEX_COLOR = euiThemeVars.euiColorPrimary; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx index a8998a134416f..7e1834e05ed4c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.test.tsx @@ -11,7 +11,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { FlattenedBucket, getFlattenedBuckets, getLegendItems } from '../helpers'; import { EMPTY_STAT } from '../../../constants'; import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; @@ -25,6 +24,9 @@ import { StorageTreemap } from '.'; import { DEFAULT_MAX_CHART_HEIGHT } from '../../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { NO_DATA_LABEL } from './translations'; import { PatternRollup } from '../../../types'; +import { FlattenedBucket } from '../types'; +import { getFlattenedBuckets } from '../utils/get_flattened_buckets'; +import { getLegendItems } from './utils/get_legend_items'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx index fbabe4412e493..5a0a7be5bab3d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/index.tsx @@ -22,12 +22,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { - FlattenedBucket, - getLayersMultiDimensional, - getLegendItems, - getPathToFlattenedBucketMap, -} from '../helpers'; import { ChartLegendItem } from './chart_legend_item'; import { NoData } from './no_data'; import { @@ -36,6 +30,10 @@ import { } from '../../indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/styles'; import { PatternRollup, SelectedIndex } from '../../../types'; import { useDataQualityContext } from '../../../data_quality_context'; +import { FlattenedBucket } from '../types'; +import { getPathToFlattenedBucketMap } from './utils/get_path_to_flattened_bucket_map'; +import { getLayersMultiDimensional } from './utils/get_layers_multi_dimensional'; +import { getLegendItems } from './utils/get_legend_items'; export const DEFAULT_MIN_CHART_HEIGHT = 240; // px export const LEGEND_WIDTH = 220; // px diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.test.ts new file mode 100644 index 0000000000000..41ce95e50cfbd --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; + +import { getFillColor } from './get_fill_color'; +import { DEFAULT_INDEX_COLOR } from '../constants'; + +describe('getFillColor', () => { + test('it returns success when `incompatible` is zero', () => { + const incompatible = 0; + + expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorSuccess); + }); + + test('it returns danger when `incompatible` is greater than 0', () => { + const incompatible = 1; + + expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorDanger); + }); + + test('it returns the default color when `incompatible` is undefined', () => { + const incompatible = undefined; + + expect(getFillColor(incompatible)).toEqual(DEFAULT_INDEX_COLOR); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.ts new file mode 100644 index 0000000000000..5c2d43a66ee15 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_fill_color.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { euiThemeVars } from '@kbn/ui-theme'; +import { DEFAULT_INDEX_COLOR } from '../constants'; + +export const getFillColor = (incompatible: number | undefined): string => { + if (incompatible === 0) { + return euiThemeVars.euiColorSuccess; + } else if (incompatible != null && incompatible > 0) { + return euiThemeVars.euiColorDanger; + } else { + return DEFAULT_INDEX_COLOR; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.test.ts new file mode 100644 index 0000000000000..aa6aee9fafc29 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { getFlattenedBuckets } from '../../utils/get_flattened_buckets'; +import { getGroupFromPath, getLayersMultiDimensional } from './get_layers_multi_dimensional'; +import { getPathToFlattenedBucketMap } from './get_path_to_flattened_bucket_map'; +import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { PatternRollup } from '../../../../types'; +import { EMPTY_STAT } from '../../../../constants'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +describe('getGroupFromPath', () => { + it('returns the expected group from the path', () => { + expect( + getGroupFromPath([ + { + index: 0, + value: '__null_small_multiples_key__', + }, + { + index: 0, + value: '__root_key__', + }, + { + index: 0, + value: 'auditbeat-*', + }, + { + index: 1, + value: 'auditbeat-custom-empty-index-1', + }, + ]) + ).toEqual('auditbeat-*'); + }); + + it('returns undefined when path is an empty array', () => { + expect(getGroupFromPath([])).toBeUndefined(); + }); + + it('returns undefined when path is an array with only one value', () => { + expect(getGroupFromPath([{ index: 0, value: '__null_small_multiples_key__' }])).toBeUndefined(); + }); +}); + +describe('getLayersMultiDimensional', () => { + const layer0FillColor = 'transparent'; + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + isILMAvailable: true, + patternRollups, + }); + const pathToFlattenedBucketMap = getPathToFlattenedBucketMap(flattenedBuckets); + + it('returns the expected number of layers', () => { + expect( + getLayersMultiDimensional({ + valueFormatter: formatBytes, + layer0FillColor, + pathToFlattenedBucketMap, + }).length + ).toEqual(2); + }); + + it('returns the expected fillLabel valueFormatter function', () => { + getLayersMultiDimensional({ + valueFormatter: formatBytes, + layer0FillColor, + pathToFlattenedBucketMap, + }).forEach((x) => expect(x.fillLabel.valueFormatter(123)).toEqual('123B')); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.ts new file mode 100644 index 0000000000000..da5947f742138 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_layers_multi_dimensional.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 { ArrayNode, Datum, Key } from '@elastic/charts'; + +import { FlattenedBucket } from '../../types'; +import { getFillColor } from './get_fill_color'; + +const groupByRollup = (d: Datum) => d.pattern; // the treemap is grouped by this field + +/** + * Extracts the first group name from the data representing the second group + */ +export const getGroupFromPath = (path: ArrayNode['path']): string | undefined => { + const OFFSET_FROM_END = 2; // The offset from the end of the path array containing the group + const groupIndex = path.length - OFFSET_FROM_END; + return groupIndex > 0 ? path[groupIndex].value : undefined; +}; + +export const getLayersMultiDimensional = ({ + valueFormatter, + layer0FillColor, + pathToFlattenedBucketMap, +}: { + valueFormatter: (value: number) => string; + layer0FillColor: string; + pathToFlattenedBucketMap: Record; +}) => { + return [ + { + fillLabel: { + valueFormatter, + }, + groupByRollup, + nodeLabel: (ilmPhase: Datum) => ilmPhase, + shape: { + fillColor: layer0FillColor, + }, + }, + { + fillLabel: { + valueFormatter, + }, + groupByRollup: (d: Datum) => d.indexName, + nodeLabel: (indexName: Datum) => indexName, + shape: { + fillColor: (indexName: Key, _sortIndex: number, node: Pick) => { + const pattern = getGroupFromPath(node.path) ?? ''; + const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`]; + + return getFillColor(flattenedBucket?.incompatible); + }, + }, + }, + ]; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.test.ts new file mode 100644 index 0000000000000..eee30b2d974cf --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.test.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; + +import { getFlattenedBuckets } from '../../utils/get_flattened_buckets'; +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 { PatternRollup } from '../../../../types'; +import { getLegendItems, getLegendItemsForPattern, getPatternLegendItem } from './get_legend_items'; + +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, +}; + +describe('getPatternLegendItem', () => { + test('it returns the expected legend item', () => { + const pattern = 'auditbeat-*'; + + expect(getPatternLegendItem({ pattern, patternRollups })).toEqual({ + color: null, + ilmPhase: null, + index: null, + pattern, + sizeInBytes: auditbeatWithAllResults.sizeInBytes, + docsCount: auditbeatWithAllResults.docsCount, + }); + }); +}); + +describe('getLegendItemsForPattern', () => { + test('it returns the expected legend items', () => { + const pattern = 'auditbeat-*'; + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + isILMAvailable: true, + patternRollups, + }); + + expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([ + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + docsCount: 19123, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + docsCount: 4, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + docsCount: 0, + }, + ]); + }); + + test('it returns the expected legend items when isILMAvailable is false', () => { + const pattern = 'auditbeat-*'; + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + isILMAvailable: false, + patternRollups, + }); + expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([ + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: null, + index: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + docsCount: 19123, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: null, + index: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + docsCount: 4, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: null, + index: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + docsCount: 0, + }, + ]); + }); +}); + +describe('getLegendItems', () => { + test('it returns the expected legend items', () => { + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + isILMAvailable: true, + patternRollups, + }); + + expect(getLegendItems({ flattenedBuckets, patterns, patternRollups })).toEqual([ + { + color: null, + ilmPhase: null, + index: null, + pattern: '.alerts-security.alerts-default', + sizeInBytes: 29717961631, + docsCount: 26093, + }, + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + sizeInBytes: 0, + docsCount: 26093, + }, + { + color: null, + ilmPhase: null, + index: null, + pattern: 'auditbeat-*', + sizeInBytes: 18820446, + docsCount: 19127, + }, + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + docsCount: 19123, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + docsCount: 4, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + docsCount: 0, + }, + { + color: null, + ilmPhase: null, + index: null, + pattern: 'packetbeat-*', + sizeInBytes: 1096520898, + docsCount: 3258632, + }, + { + color: euiThemeVars.euiColorPrimary, + ilmPhase: 'hot', + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 584326147, + docsCount: 1630289, + }, + { + color: euiThemeVars.euiColorPrimary, + ilmPhase: 'hot', + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 512194751, + docsCount: 1628343, + }, + ]); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.ts new file mode 100644 index 0000000000000..16816dcf2a3c6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_legend_items.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { orderBy } from 'lodash/fp'; + +import { PatternRollup } from '../../../../types'; +import { FlattenedBucket, LegendItem } from '../../types'; +import { getFillColor } from './get_fill_color'; +import { getPatternDocsCount, getPatternSizeInBytes } from './stats'; + +export const getLegendItemsForPattern = ({ + pattern, + flattenedBuckets, +}: { + pattern: string; + flattenedBuckets: FlattenedBucket[]; +}): LegendItem[] => + orderBy( + ['sizeInBytes'], + ['desc'], + flattenedBuckets + .filter((x) => x.pattern === pattern) + .map((flattenedBucket) => ({ + color: getFillColor(flattenedBucket.incompatible), + ilmPhase: flattenedBucket.ilmPhase ?? null, + index: flattenedBucket.indexName ?? null, + pattern: flattenedBucket.pattern, + sizeInBytes: flattenedBucket.sizeInBytes, + docsCount: flattenedBucket.docsCount, + })) + ); + +export const getPatternLegendItem = ({ + pattern, + patternRollups, +}: { + pattern: string; + patternRollups: Record; +}): LegendItem => ({ + color: null, + ilmPhase: null, + index: null, + pattern, + sizeInBytes: getPatternSizeInBytes({ pattern, patternRollups }), + docsCount: getPatternDocsCount({ pattern, patternRollups }), +}); + +export const getLegendItems = ({ + patterns, + flattenedBuckets, + patternRollups, +}: { + patterns: string[]; + flattenedBuckets: FlattenedBucket[]; + patternRollups: Record; +}): LegendItem[] => + patterns.reduce( + (acc, pattern) => [ + ...acc, + getPatternLegendItem({ pattern, patternRollups }), + ...getLegendItemsForPattern({ pattern, flattenedBuckets }), + ], + [] + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.test.ts new file mode 100644 index 0000000000000..f7a3be93fcc85 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PatternRollup } from '../../../../types'; +import { getFlattenedBuckets } from '../../utils/get_flattened_buckets'; +import { getPathToFlattenedBucketMap } from './get_path_to_flattened_bucket_map'; +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'; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +describe('helpers', () => { + describe('getPathToFlattenedBucketMap', () => { + test('it returns the expected map', () => { + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + isILMAvailable: true, + patternRollups, + }); + + expect(getPathToFlattenedBucketMap(flattenedBuckets)).toEqual({ + '.alerts-security.alerts-default.internal.alerts-security.alerts-default-000001': { + pattern: '.alerts-security.alerts-default', + indexName: '.internal.alerts-security.alerts-default-000001', + ilmPhase: 'hot', + incompatible: 0, + sizeInBytes: 0, + docsCount: 26093, + }, + 'auditbeat-*.ds-auditbeat-8.6.1-2023.02.07-000001': { + pattern: 'auditbeat-*', + indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', + ilmPhase: 'hot', + incompatible: 0, + sizeInBytes: 18791790, + docsCount: 19123, + }, + 'auditbeat-*auditbeat-custom-empty-index-1': { + pattern: 'auditbeat-*', + indexName: 'auditbeat-custom-empty-index-1', + ilmPhase: 'unmanaged', + incompatible: 1, + sizeInBytes: 247, + docsCount: 0, + }, + 'auditbeat-*auditbeat-custom-index-1': { + pattern: 'auditbeat-*', + indexName: 'auditbeat-custom-index-1', + ilmPhase: 'unmanaged', + incompatible: 3, + sizeInBytes: 28409, + docsCount: 4, + }, + 'packetbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001': { + pattern: 'packetbeat-*', + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + ilmPhase: 'hot', + sizeInBytes: 512194751, + docsCount: 1628343, + }, + 'packetbeat-*.ds-packetbeat-8.5.3-2023.02.04-000001': { + docsCount: 1630289, + pattern: 'packetbeat-*', + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + ilmPhase: 'hot', + sizeInBytes: 584326147, + }, + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.ts new file mode 100644 index 0000000000000..4afbe4c1b73cd --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/get_path_to_flattened_bucket_map.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 { FlattenedBucket } from '../../types'; + +export const getPathToFlattenedBucketMap = ( + flattenedBuckets: FlattenedBucket[] +): Record => + flattenedBuckets.reduce>( + (acc, { pattern, indexName, ...remaining }) => ({ + ...acc, + [`${pattern}${indexName}`]: { pattern, indexName, ...remaining }, + }), + {} + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.test.ts new file mode 100644 index 0000000000000..b2b31eac00ab0 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PatternRollup } from '../../../../types'; +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 { getPatternSizeInBytes } from './stats'; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +/** a valid `PatternRollup` that has an undefined `sizeInBytes` */ +const noSizeInBytes: Record = { + 'valid-*': { + docsCount: 19127, + error: null, + ilmExplain: null, + ilmExplainPhaseCounts: { + hot: 1, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 2, + }, + indices: 3, + pattern: 'valid-*', + results: undefined, + sizeInBytes: undefined, // <-- + stats: null, + }, +}; + +describe('getPatternSizeInBytes', () => { + test('it returns the expected size when the pattern exists in the rollup', () => { + const pattern = 'auditbeat-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups })).toEqual( + auditbeatWithAllResults.sizeInBytes + ); + }); + + test('it returns undefined when the pattern exists in the rollup, but does not have a sizeInBytes', () => { + const pattern = 'valid-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups: noSizeInBytes })).toBeUndefined(); + }); + + test('it returns undefined when the pattern does NOT exist in the rollup', () => { + const pattern = 'does-not-exist-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups })).toBeUndefined(); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.ts new file mode 100644 index 0000000000000..9d999266a40f5 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/storage_treemap/utils/stats.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 { PatternRollup } from '../../../../types'; + +export const getPatternSizeInBytes = ({ + pattern, + patternRollups, +}: { + pattern: string; + patternRollups: Record; +}): number | undefined => { + if (patternRollups[pattern] != null) { + return patternRollups[pattern].sizeInBytes; + } else { + return undefined; + } +}; + +export const getPatternDocsCount = ({ + pattern, + patternRollups, +}: { + pattern: string; + patternRollups: Record; +}): number => { + if (patternRollups[pattern] != null) { + return patternRollups[pattern].docsCount ?? 0; + } else { + return 0; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/types.ts similarity index 56% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/types.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/types.ts index e44300859bffd..a1507d1ad9bcb 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/types.ts @@ -5,15 +5,20 @@ * 2.0. */ -import { IlmPhase } from '../../../types'; - -export interface IndexSummaryTableItem { +export interface LegendItem { + color: string | null; + ilmPhase: string | null; + index: string | null; + pattern: string; + sizeInBytes: number | undefined; docsCount: number; +} + +export interface FlattenedBucket { + ilmPhase: string | undefined; incompatible: number | undefined; - indexName: string; - ilmPhase: IlmPhase | undefined; + indexName: string | undefined; pattern: string; - patternDocsCount: number; sizeInBytes: number | undefined; - checkedAt: number | undefined; + docsCount: number; } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.test.ts new file mode 100644 index 0000000000000..7aaff4888186a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +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 { PatternRollup } from '../../../types'; +import { getFlattenedBuckets } from './get_flattened_buckets'; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +describe('getFlattenedBuckets', () => { + test('it returns the expected flattened buckets', () => { + expect( + getFlattenedBuckets({ + ilmPhases, + isILMAvailable: true, + patternRollups, + }) + ).toEqual([ + { + ilmPhase: 'hot', + incompatible: 0, + indexName: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + sizeInBytes: 0, + docsCount: 26093, + }, + { + ilmPhase: 'hot', + incompatible: 0, + indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + docsCount: 19123, + }, + { + ilmPhase: 'unmanaged', + incompatible: 1, + indexName: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + docsCount: 0, + }, + { + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + docsCount: 4, + }, + { + ilmPhase: 'hot', + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 512194751, + docsCount: 1628343, + }, + { + ilmPhase: 'hot', + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 584326147, + docsCount: 1630289, + }, + ]); + }); + + test('it returns the expected flattened buckets when isILMAvailable is false', () => { + expect( + getFlattenedBuckets({ + ilmPhases, + isILMAvailable: false, + patternRollups, + }) + ).toEqual([ + { + docsCount: 26093, + ilmPhase: undefined, + incompatible: 0, + indexName: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + sizeInBytes: 0, + }, + { + docsCount: 19123, + ilmPhase: undefined, + incompatible: 0, + indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + }, + { + docsCount: 0, + ilmPhase: undefined, + incompatible: 1, + indexName: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + }, + { + docsCount: 4, + ilmPhase: undefined, + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + }, + { + docsCount: 1628343, + ilmPhase: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 512194751, + }, + { + docsCount: 1630289, + ilmPhase: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 584326147, + }, + ]); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.ts new file mode 100644 index 0000000000000..7363ae1df2c33 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/storage_details/utils/get_flattened_buckets.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PatternRollup } from '../../../types'; +import { getIlmPhase } from '../../../utils/get_ilm_phase'; +import { getDocsCount, getSizeInBytes } from '../../../utils/stats'; +import { FlattenedBucket } from '../types'; + +export const getFlattenedBuckets = ({ + ilmPhases, + isILMAvailable, + patternRollups, +}: { + ilmPhases: string[]; + isILMAvailable: boolean; + patternRollups: Record; +}): FlattenedBucket[] => + Object.values(patternRollups).reduce((acc, patternRollup) => { + // enables fast lookup of valid phase names: + const ilmPhasesMap = ilmPhases.reduce>( + (phasesMap, phase) => ({ ...phasesMap, [phase]: 0 }), + {} + ); + const { ilmExplain, pattern, results, stats } = patternRollup; + + if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) { + return [ + ...acc, + ...Object.entries(stats).reduce((validStats, [indexName]) => { + const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable); + const isSelectedPhase = + (isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) || + !isILMAvailable; + + if (isSelectedPhase) { + const incompatible = + results != null && results[indexName] != null + ? results[indexName].incompatible + : undefined; + const sizeInBytes = getSizeInBytes({ indexName, stats }); + const docsCount = getDocsCount({ stats, indexName }); + return [ + ...validStats, + { + ilmPhase, + incompatible, + indexName, + pattern, + sizeInBytes, + docsCount, + }, + ]; + } else { + return validStats; + } + }, []), + ]; + } + + return acc; + }, []); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts deleted file mode 100644 index 2b37622baa655..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts +++ /dev/null @@ -1,107 +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 { getAllIndicesToCheck, getIndexDocsCountFromRollup, getIndexToCheck } from './helpers'; -import { mockPacketbeatPatternRollup } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; - -const patternIndexNames: Record = { - 'packetbeat-*': [ - '.ds-packetbeat-8.6.1-2023.02.04-000001', - '.ds-packetbeat-8.5.3-2023.02.04-000001', - ], - 'auditbeat-*': [ - 'auditbeat-7.17.9-2023.02.13-000001', - 'auditbeat-custom-index-1', - '.ds-auditbeat-8.6.1-2023.02.13-000001', - ], - 'logs-*': [ - '.ds-logs-endpoint.alerts-default-2023.02.24-000001', - '.ds-logs-endpoint.events.process-default-2023.02.24-000001', - ], - 'remote:*': [], - '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], -}; - -describe('helpers', () => { - describe('getIndexToCheck', () => { - test('it returns the expected `IndexToCheck`', () => { - expect( - getIndexToCheck({ - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - }) - ).toEqual({ - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - }); - }); - }); - - describe('getAllIndicesToCheck', () => { - test('it returns the sorted collection of `IndexToCheck`', () => { - expect(getAllIndicesToCheck(patternIndexNames)).toEqual([ - { - indexName: '.internal.alerts-security.alerts-default-000001', - pattern: '.alerts-security.alerts-default', - }, - { - indexName: 'auditbeat-custom-index-1', - pattern: 'auditbeat-*', - }, - { - indexName: 'auditbeat-7.17.9-2023.02.13-000001', - pattern: 'auditbeat-*', - }, - { - indexName: '.ds-auditbeat-8.6.1-2023.02.13-000001', - pattern: 'auditbeat-*', - }, - { - indexName: '.ds-logs-endpoint.events.process-default-2023.02.24-000001', - pattern: 'logs-*', - }, - { - indexName: '.ds-logs-endpoint.alerts-default-2023.02.24-000001', - pattern: 'logs-*', - }, - { - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - pattern: 'packetbeat-*', - }, - { - indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', - pattern: 'packetbeat-*', - }, - ]); - }); - }); - - describe('getIndexDocsCountFromRollup', () => { - test('it returns the expected count when the `patternRollup` has `stats`', () => { - expect( - getIndexDocsCountFromRollup({ - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - patternRollup: mockPacketbeatPatternRollup, - }) - ).toEqual(1628343); - }); - - test('it returns zero when the `patternRollup` `stats` is null', () => { - const patternRollup = { - ...mockPacketbeatPatternRollup, - stats: null, // <-- - }; - - expect( - getIndexDocsCountFromRollup({ - indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', - patternRollup, - }) - ).toEqual(0); - }); - }); -}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx index e2851ee4b3761..9e90b28e71a9e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; -import { getAllIndicesToCheck } from './helpers'; +import { getAllIndicesToCheck } from './utils/get_all_indices_to_check'; import { useResultsRollupContext } from '../../../contexts/results_rollup_context'; import { checkIndex } from '../../../utils/check_index'; import { useDataQualityContext } from '../../../data_quality_context'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/translations.ts deleted file mode 100644 index 219d1039ac290..0000000000000 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/translations.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const AN_ERROR_OCCURRED_CHECKING_INDEX = (indexName: string) => - i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.checkAllErrorCheckingIndexMessage', - { - values: { indexName }, - defaultMessage: 'An error occurred checking index {indexName}', - } - ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.test.ts new file mode 100644 index 0000000000000..3a3bbf5880455 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getAllIndicesToCheck, getIndexToCheck } from './get_all_indices_to_check'; + +const patternIndexNames: Record = { + 'packetbeat-*': [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + ], + 'auditbeat-*': [ + 'auditbeat-7.17.9-2023.02.13-000001', + 'auditbeat-custom-index-1', + '.ds-auditbeat-8.6.1-2023.02.13-000001', + ], + 'logs-*': [ + '.ds-logs-endpoint.alerts-default-2023.02.24-000001', + '.ds-logs-endpoint.events.process-default-2023.02.24-000001', + ], + 'remote:*': [], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], +}; + +describe('getIndexToCheck', () => { + test('it returns the expected `IndexToCheck`', () => { + expect( + getIndexToCheck({ + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }) + ).toEqual({ + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }); + }); +}); + +describe('getAllIndicesToCheck', () => { + test('it returns the sorted collection of `IndexToCheck`', () => { + expect(getAllIndicesToCheck(patternIndexNames)).toEqual([ + { + indexName: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + }, + { + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }, + { + indexName: 'auditbeat-7.17.9-2023.02.13-000001', + pattern: 'auditbeat-*', + }, + { + indexName: '.ds-auditbeat-8.6.1-2023.02.13-000001', + pattern: 'auditbeat-*', + }, + { + indexName: '.ds-logs-endpoint.events.process-default-2023.02.24-000001', + pattern: 'logs-*', + }, + { + indexName: '.ds-logs-endpoint.alerts-default-2023.02.24-000001', + pattern: 'logs-*', + }, + { + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + }, + { + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + }, + ]); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.ts similarity index 73% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.ts index dfbc0f69f82aa..dbe1b113bc9aa 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/check_all/utils/get_all_indices_to_check.ts @@ -7,8 +7,7 @@ import { orderBy } from 'lodash/fp'; -import type { IndexToCheck, MeteringStatsIndex, PatternRollup } from '../../../types'; -import { getDocsCount } from '../../../utils/stats'; +import type { IndexToCheck } from '../../../../types'; export const getIndexToCheck = ({ indexName, @@ -45,18 +44,3 @@ export const getAllIndicesToCheck = ( return [...acc, ...sortedIndicesToCheck]; }, []); }; - -export const getIndexDocsCountFromRollup = ({ - indexName, - patternRollup, -}: { - indexName: string; - patternRollup: PatternRollup; -}): number => { - const stats: Record | null = patternRollup?.stats ?? null; - - return getDocsCount({ - indexName, - stats, - }); -}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx index d0037032172c3..20fbd0b2478b0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_summary/summary_actions/index.tsx @@ -22,16 +22,14 @@ import { getSummaryTableMarkdownHeader, getSummaryTableMarkdownRow, } from '../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers'; -import { - defaultSort, - getSummaryTableItems, -} from '../../data_quality_details/indices_details/pattern/helpers'; import type { DataQualityCheckResult, IndexToCheck, PatternRollup } from '../../types'; import { useDataQualityContext } from '../../data_quality_context'; import { useResultsRollupContext } from '../../contexts/results_rollup_context'; import { Actions } from '../../actions'; import { getErrorSummaries } from './utils/get_error_summaries'; import { getSizeInBytes } from '../../utils/stats'; +import { getSummaryTableItems } from '../../utils/get_summary_table_items'; +import { defaultSort } from '../../constants'; const StyledActionsContainerFlexItem = styled(EuiFlexItem)` margin-top: auto; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx index eb8cd670d70c5..e2571b5d0a75c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx @@ -34,10 +34,6 @@ import type { PatternRollup, TelemetryEvents, } from '../../types'; -import { - getIlmPhase, - getIndexIncompatible, -} from '../../data_quality_details/indices_details/pattern/helpers'; import { getIncompatibleMappingsFields, getIncompatibleValuesFields, @@ -45,7 +41,8 @@ import { } from '../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers'; import { UseResultsRollupReturnValue } from './types'; import { useIsMounted } from '../use_is_mounted'; -import { getDocsCount, getSizeInBytes } from '../../utils/stats'; +import { getDocsCount, getIndexIncompatible, getSizeInBytes } from '../../utils/stats'; +import { getIlmPhase } from '../../utils/get_ilm_phase'; interface Props { ilmPhases: string[]; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts index 22b7eb6db4d8d..e826730df89cc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/get_pattern_rollups_with_latest_check_result.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { getIndexDocsCountFromRollup } from '../../../data_quality_summary/summary_actions/check_all/helpers'; -import { getIlmPhase } from '../../../data_quality_details/indices_details/pattern/helpers'; import { getAllIncompatibleMarkdownComments } from '../../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/index_check_fields/tabs/incompatible_tab/helpers'; import { getSizeInBytes } from '../../../utils/stats'; import type { IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../../../types'; +import { getIndexDocsCountFromRollup } from './stats'; +import { getIlmPhase } from '../../../utils/get_ilm_phase'; export const getPatternRollupsWithLatestCheckResult = ({ error, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.test.ts index 7f28c6bcd1727..0d48a794670d5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.test.ts @@ -15,6 +15,7 @@ import { import { mockStats } from '../../../mock/stats/mock_stats'; import { DataQualityCheckResult, PatternRollup } from '../../../types'; import { + getIndexDocsCountFromRollup, getIndexId, getTotalDocsCount, getTotalIncompatible, @@ -229,3 +230,28 @@ describe('getIndexId', () => { ); }); }); + +describe('getIndexDocsCountFromRollup', () => { + test('it returns the expected count when the `patternRollup` has `stats`', () => { + expect( + getIndexDocsCountFromRollup({ + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + patternRollup: mockPacketbeatPatternRollup, + }) + ).toEqual(1628343); + }); + + test('it returns zero when the `patternRollup` `stats` is null', () => { + const patternRollup = { + ...mockPacketbeatPatternRollup, + stats: null, // <-- + }; + + expect( + getIndexDocsCountFromRollup({ + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + patternRollup, + }) + ).toEqual(0); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.ts index c3a44f04471ea..80965c706d9f3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/stats.ts @@ -6,7 +6,11 @@ */ import { DataQualityCheckResult, MeteringStatsIndex, PatternRollup } from '../../../types'; -import { getTotalPatternIncompatible, getTotalPatternIndicesChecked } from '../../../utils/stats'; +import { + getDocsCount, + getTotalPatternIncompatible, + getTotalPatternIndicesChecked, +} from '../../../utils/stats'; export const getTotalPatternSameFamily = ( results: Record | undefined @@ -98,3 +102,18 @@ export const getIndexId = ({ indexName: string; stats: Record | null; }): string | null | undefined => stats && stats[indexName]?.uuid; + +export const getIndexDocsCountFromRollup = ({ + indexName, + patternRollup, +}: { + indexName: string; + patternRollup: PatternRollup; +}): number => { + const stats: Record | null = patternRollup?.stats ?? null; + + return getDocsCount({ + indexName, + stats, + }); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts index 12f45c192aa25..1c56d25367e3a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_check_state/index.ts @@ -7,10 +7,6 @@ import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; -import { - getMappingsProperties, - getSortedPartitionedFieldMetadata, -} from '../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers'; import { mockMappingsResponse } from '../../mock/mappings_response/mock_mappings_response'; import { UseIndicesCheckCheckState } from '../../hooks/use_indices_check/types'; import { getUnallowedValues } from '../../utils/fetch_unallowed_values'; @@ -18,6 +14,7 @@ import { getUnallowedValueRequestItems } from '../../utils/get_unallowed_value_r import { EcsFlatTyped } from '../../constants'; import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_unallowed_values'; import { UnallowedValueSearchResult } from '../../types'; +import { getMappingsProperties, getSortedPartitionedFieldMetadata } from '../../utils/metadata'; export const getCheckState = ( indexName: string, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts index 3497aa89d97ee..904ab05a77007 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/translations.ts @@ -296,3 +296,12 @@ export const DATA_QUALITY_DASHBOARD_CONVERSATION_ID = i18n.translate( defaultMessage: 'Data Quality dashboard', } ); + +export const AN_ERROR_OCCURRED_CHECKING_INDEX = (indexName: string) => + i18n.translate( + 'securitySolutionPackages.ecsDataQualityDashboard.checkAllErrorCheckingIndexMessage', + { + values: { indexName }, + defaultMessage: 'An error occurred checking index {indexName}', + } + ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts index 916d0f377bf85..08c964d423101 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/types.ts @@ -276,3 +276,14 @@ export interface TelemetryEvents { reportDataQualityIndexChecked?: ReportDataQualityIndexChecked; reportDataQualityCheckAllCompleted?: ReportDataQualityCheckAllCompleted; } + +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_panel/utils/check_index.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.test.ts index 2b90f67966c77..77538a60c65c1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.test.ts @@ -9,14 +9,11 @@ import { checkIndex, EMPTY_PARTITIONED_FIELD_METADATA } from './check_index'; 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_details/indices_details/pattern/index_check_flyout/index_properties/helpers'; import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; import { getUnallowedValues } from './fetch_unallowed_values'; import { getUnallowedValueRequestItems } from './get_unallowed_value_request_items'; import { EcsFlatTyped, EMPTY_STAT } from '../constants'; +import { getMappingsProperties, getSortedPartitionedFieldMetadata } from './metadata'; let mockFetchMappings = jest.fn( (_: { abortController: AbortController; patternOrIndexName: string }) => diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.ts index 76094c97f5667..a577ca15d0ad8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/check_index.ts @@ -13,11 +13,7 @@ import { import { v4 as uuidv4 } from 'uuid'; import { getUnallowedValueRequestItems } from './get_unallowed_value_request_items'; -import { - getMappingsProperties, - getSortedPartitionedFieldMetadata, -} from '../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/helpers'; -import * as i18n from '../data_quality_summary/summary_actions/check_all/translations'; +import * as i18n from '../translations'; import type { OnCheckCompleted, PartitionedFieldMetadata, @@ -27,6 +23,7 @@ import type { import { fetchMappings } from './fetch_mappings'; import { fetchUnallowedValues, getUnallowedValues } from './fetch_unallowed_values'; import { EcsFlatTyped } from '../constants'; +import { getMappingsProperties, getSortedPartitionedFieldMetadata } from './metadata'; export const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { all: [], diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.test.ts new file mode 100644 index 0000000000000..dd92e3700c18a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + IlmExplainLifecycleLifecycleExplainManaged, + IlmExplainLifecycleLifecycleExplainUnmanaged, +} from '@elastic/elasticsearch/lib/api/types'; + +import { getIlmPhase } from './get_ilm_phase'; + +const hot: IlmExplainLifecycleLifecycleExplainManaged = { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '3.98d', + lifecycle_date_millis: 1675536751379, + age: '3.98d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, +}; + +const warm = { + ...hot, + phase: 'warm', +}; +const cold = { + ...hot, + phase: 'cold', +}; +const frozen = { + ...hot, + phase: 'frozen', +}; +const other = { + ...hot, + phase: 'other', // not a valid phase +}; + +const managed: Record = { + hot, + warm, + cold, + frozen, +}; + +const unmanaged: IlmExplainLifecycleLifecycleExplainUnmanaged = { + index: 'michael', + managed: false, +}; + +describe('getIlmPhase', () => { + const isILMAvailable = true; + test('it returns undefined when the `ilmExplainRecord` is undefined', () => { + expect(getIlmPhase(undefined, isILMAvailable)).toBeUndefined(); + }); + + describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainManaged` record', () => { + Object.keys(managed).forEach((phase) => + test(`it returns the expected phase when 'phase' is '${phase}'`, () => { + expect(getIlmPhase(managed[phase], isILMAvailable)).toEqual(phase); + }) + ); + + test(`it returns undefined when the 'phase' is unknown`, () => { + expect(getIlmPhase(other, isILMAvailable)).toBeUndefined(); + }); + }); + + describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainUnmanaged` record', () => { + test('it returns `unmanaged`', () => { + expect(getIlmPhase(unmanaged, isILMAvailable)).toEqual('unmanaged'); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.ts new file mode 100644 index 0000000000000..78e9bfb98f2d6 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_ilm_phase.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 { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { IlmPhase } from '../types'; + +export const getIlmPhase = ( + ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined, + isILMAvailable: boolean +): IlmPhase | undefined => { + if (ilmExplainRecord == null || !isILMAvailable) { + return undefined; + } + + if ('phase' in ilmExplainRecord) { + const phase = ilmExplainRecord.phase; + + switch (phase) { + case 'hot': + return 'hot'; + case 'warm': + return 'warm'; + case 'cold': + return 'cold'; + case 'frozen': + return 'frozen'; + default: + return undefined; + } + } else { + return 'unmanaged'; + } +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.test.ts new file mode 100644 index 0000000000000..7bfde1d8d7844 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultSort } from '../constants'; +import { mockIlmExplain } from '../mock/ilm_explain/mock_ilm_explain'; +import { mockStats } from '../mock/stats/mock_stats'; +import { DataQualityCheckResult } from '../types'; +import { getSummaryTableItems } from './get_summary_table_items'; + +describe('getSummaryTableItems', () => { + const indexNames = [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + 'auditbeat-custom-index-1', + ]; + const pattern = 'auditbeat-*'; + const patternDocsCount = 4; + const results: Record = { + 'auditbeat-custom-index-1': { + docsCount: 4, + error: null, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + markdownComments: [ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\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),\n`theory` (1) |\n\n', + ], + pattern: 'auditbeat-*', + sameFamily: 0, + checkedAt: 1706526408000, + }, + }; + const isILMAvailable = true; + + test('it returns the expected summary table items', () => { + expect( + getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames, + isILMAvailable, + pattern, + patternDocsCount, + results, + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: mockStats, + }) + ).toEqual([ + { + docsCount: 1630289, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 733175040, + checkedAt: undefined, + }, + { + docsCount: 1628343, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 731583142, + checkedAt: undefined, + }, + { + docsCount: 4, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 28413, + checkedAt: 1706526408000, + }, + ]); + }); + + test('it returns the expected summary table items when isILMAvailable is false', () => { + expect( + getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames, + isILMAvailable: false, + pattern, + patternDocsCount, + results, + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: mockStats, + }) + ).toEqual([ + { + docsCount: 1630289, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 733175040, + checkedAt: undefined, + }, + { + docsCount: 1628343, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 731583142, + checkedAt: undefined, + }, + { + docsCount: 4, + ilmPhase: undefined, + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 28413, + checkedAt: 1706526408000, + }, + ]); + }); + + test('it returns the expected summary table items when `sortByDirection` is ascending', () => { + expect( + getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames, + isILMAvailable, + pattern, + patternDocsCount, + results, + sortByColumn: defaultSort.sort.field, + sortByDirection: 'asc', // <-- ascending + stats: mockStats, + }) + ).toEqual([ + { + docsCount: 4, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 28413, + checkedAt: 1706526408000, + }, + { + docsCount: 1628343, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 731583142, + checkedAt: undefined, + }, + { + docsCount: 1630289, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 733175040, + checkedAt: undefined, + }, + ]); + }); + + test('it returns the expected summary table items when data is unavailable', () => { + expect( + getSummaryTableItems({ + ilmExplain: null, // <-- no data + indexNames, + isILMAvailable, + pattern, + patternDocsCount, + results: undefined, // <-- no data + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: null, // <-- no data + }) + ).toEqual([ + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: undefined, + checkedAt: undefined, + }, + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: undefined, + checkedAt: undefined, + }, + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: undefined, + checkedAt: undefined, + }, + ]); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.ts new file mode 100644 index 0000000000000..7e518a739b251 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/get_summary_table_items.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; +import { orderBy } from 'lodash/fp'; + +import { DataQualityCheckResult, IndexSummaryTableItem, MeteringStatsIndex } from '../types'; +import { getIlmPhase } from './get_ilm_phase'; +import { getDocsCount, getIndexIncompatible, getSizeInBytes } from './stats'; + +export const getSummaryTableItems = ({ + ilmExplain, + indexNames, + isILMAvailable, + pattern, + patternDocsCount, + results, + sortByColumn, + sortByDirection, + stats, +}: { + ilmExplain: Record | null; + indexNames: string[]; + isILMAvailable: boolean; + pattern: string; + patternDocsCount: number; + results: Record | undefined; + sortByColumn: string; + sortByDirection: 'desc' | 'asc'; + stats: Record | null; +}): IndexSummaryTableItem[] => { + const summaryTableItems = indexNames.map((indexName) => ({ + docsCount: getDocsCount({ stats, indexName }), + incompatible: getIndexIncompatible({ indexName, results }), + indexName, + ilmPhase: + isILMAvailable && ilmExplain != null + ? getIlmPhase(ilmExplain[indexName], isILMAvailable) + : undefined, + pattern, + patternDocsCount, + sizeInBytes: getSizeInBytes({ stats, indexName }), + checkedAt: results?.[indexName]?.checkedAt, + })); + + return orderBy([sortByColumn], [sortByDirection], summaryTableItems); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts similarity index 66% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.test.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts index d59e4821ed1c8..b85f4a7516dbb 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.test.ts @@ -6,21 +6,19 @@ */ import { omit } from 'lodash/fp'; -import { - EnrichedFieldMetadata, - PartitionedFieldMetadata, - UnallowedValueCount, -} from '../../../../../../types'; -import { mockMappingsProperties } from '../../../../../../mock/mappings_properties/mock_mappings_properties'; +import { EnrichedFieldMetadata, PartitionedFieldMetadata, UnallowedValueCount } from '../types'; +import { mockMappingsProperties } from '../mock/mappings_properties/mock_mappings_properties'; import { FieldType, getEnrichedFieldMetadata, getFieldTypes, + getMappingsProperties, getMissingTimestampFieldMetadata, getPartitionedFieldMetadata, + getSortedPartitionedFieldMetadata, isMappingCompatible, } from './metadata'; -import { EcsFlatTyped } from '../../../../../../constants'; +import { EcsFlatTyped } from '../constants'; import { hostNameWithTextMapping, hostNameKeyword, @@ -31,7 +29,8 @@ import { sourcePort, timestamp, eventCategoryWithUnallowedValues, -} from '../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +} from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { mockIndicesGetMappingIndexMappingRecords } from '../mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record'; describe('getFieldTypes', () => { const expected = [ @@ -400,3 +399,239 @@ describe('getPartitionedFieldMetadata', () => { expect(getPartitionedFieldMetadata(enrichedFieldMetadata)).toEqual(expected); }); }); + +describe('getSortedPartitionedFieldMetadata', () => { + test('it returns null when mappings are loading', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: true, // <-- + mappingsProperties: mockMappingsProperties, + unallowedValues: {}, + }) + ).toBeNull(); + }); + + test('it returns null when `unallowedValues` is null', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties: mockMappingsProperties, + unallowedValues: null, // <-- + }) + ).toBeNull(); + }); + + describe('when `mappingsProperties` is unknown', () => { + const incompatibleFieldMetadata = { + ...EcsFlatTyped['@timestamp'], + hasEcsMetadata: true, + indexFieldName: '@timestamp', + indexFieldType: '-', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + }; + const expected = { + all: [incompatibleFieldMetadata], + custom: [], + ecsCompliant: [], + incompatible: [incompatibleFieldMetadata], + sameFamily: [], + }; + + test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is undefined', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties: undefined, // <-- + unallowedValues: {}, + }) + ).toEqual(expected); + }); + + test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is null', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties: null, // <-- + unallowedValues: {}, + }) + ).toEqual(expected); + }); + }); + + test('it returns the expected sorted field metadata', () => { + const unallowedValues = { + 'event.category': [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + 'event.kind': [], + 'event.outcome': [], + 'event.type': [], + }; + + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: EcsFlatTyped, + loadingMappings: false, + mappingsProperties: mockMappingsProperties, + unallowedValues, + }) + ).toMatchObject({ + all: expect.arrayContaining([ + expect.objectContaining({ + name: expect.any(String), + flat_name: expect.any(String), + dashed_name: expect.any(String), + description: expect.any(String), + hasEcsMetadata: true, + isEcsCompliant: expect.any(Boolean), + isInSameFamily: expect.any(Boolean), + }), + ]), + ecsCompliant: expect.arrayContaining([ + expect.objectContaining({ + name: expect.any(String), + flat_name: expect.any(String), + dashed_name: expect.any(String), + description: expect.any(String), + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }), + ]), + custom: expect.arrayContaining([ + expect.objectContaining({ + indexFieldName: expect.any(String), + indexFieldType: expect.any(String), + indexInvalidValues: expect.any(Array), + hasEcsMetadata: expect.any(Boolean), + isEcsCompliant: expect.any(Boolean), + isInSameFamily: expect.any(Boolean), + }), + ]), + incompatible: expect.arrayContaining([ + expect.objectContaining({ + name: expect.any(String), + flat_name: expect.any(String), + dashed_name: expect.any(String), + description: expect.any(String), + hasEcsMetadata: expect.any(Boolean), + isEcsCompliant: false, + isInSameFamily: false, + }), + ]), + sameFamily: [], + }); + }); +}); + +describe('getMappingsProperties', () => { + test('it returns the expected mapping properties', () => { + expect( + getMappingsProperties({ + indexes: mockIndicesGetMappingIndexMappingRecords, + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual({ + '@timestamp': { + type: 'date', + }, + event: { + properties: { + category: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + host: { + properties: { + name: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + }, + }, + some: { + properties: { + field: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + }, + }, + source: { + properties: { + ip: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + port: { + type: 'long', + }, + }, + }, + }); + }); + + test('it returns null when `indexes` is null', () => { + expect( + getMappingsProperties({ + indexes: null, // <-- + indexName: 'auditbeat-custom-index-1', + }) + ).toBeNull(); + }); + + test('it returns null when `indexName` does not exist in `indexes`', () => { + expect( + getMappingsProperties({ + indexes: mockIndicesGetMappingIndexMappingRecords, + indexName: 'does-not-exist', // <-- + }) + ).toBeNull(); + }); + + test('it returns null when `properties` does not exist in the mappings', () => { + const missingProperties = { + ...mockIndicesGetMappingIndexMappingRecords, + foozle: { + mappings: {}, // <-- does not have a `properties` + }, + }; + + expect( + getMappingsProperties({ + indexes: missingProperties, + indexName: 'foozle', + }) + ).toBeNull(); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts similarity index 68% rename from x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.ts rename to x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts index 87adf1e2314e1..c2fb750e4f0a8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/metadata.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/metadata.ts @@ -5,16 +5,20 @@ * 2.0. */ -import { has } from 'lodash/fp'; +import { + IndicesGetMappingIndexMappingRecord, + MappingProperty, +} from '@elastic/elasticsearch/lib/api/types'; +import { has, sortBy } from 'lodash/fp'; -import { EcsFlatTyped } from '../../../../../../constants'; +import { EMPTY_METADATA, EcsFlatTyped } from '../constants'; import { EcsBasedFieldMetadata, EnrichedFieldMetadata, PartitionedFieldMetadata, UnallowedValueCount, -} from '../../../../../../types'; -import { getIsInSameFamily } from './get_is_in_same_family'; +} from '../types'; +import { getIsInSameFamily } from '../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/utils/get_is_in_same_family'; export const getPartitionedFieldMetadata = ( enrichedFieldMetadata: EnrichedFieldMetadata[] @@ -156,6 +160,49 @@ export const getEnrichedFieldMetadata = ({ } }; +export const getSortedPartitionedFieldMetadata = ({ + ecsMetadata, + loadingMappings, + mappingsProperties, + unallowedValues, +}: { + ecsMetadata: EcsFlatTyped; + loadingMappings: boolean; + mappingsProperties: Record | null | undefined; + unallowedValues: Record | null; +}): PartitionedFieldMetadata | null => { + if (loadingMappings || unallowedValues == null) { + return null; + } + + // this covers scenario when we try to check an empty index + // or index without required @timestamp field in the mapping + // + // we create an artifical incompatible timestamp field metadata + // so that we can signal to user that the incompatibility is due to missing timestamp + if (mappingsProperties == null) { + const missingTimestampFieldMetadata = getMissingTimestampFieldMetadata(); + return { + ...EMPTY_METADATA, + all: [missingTimestampFieldMetadata], + incompatible: [missingTimestampFieldMetadata], + }; + } + + const fieldTypes = getFieldTypes(mappingsProperties); + + const enrichedFieldMetadata = sortBy( + 'indexFieldName', + fieldTypes.map((fieldMetadata) => + getEnrichedFieldMetadata({ ecsMetadata, fieldMetadata, unallowedValues }) + ) + ); + + const partitionedFieldMetadata = getPartitionedFieldMetadata(enrichedFieldMetadata); + + return partitionedFieldMetadata; +}; + export const getMissingTimestampFieldMetadata = (): EcsBasedFieldMetadata => ({ ...EcsFlatTyped['@timestamp'], hasEcsMetadata: true, @@ -165,3 +212,17 @@ export const getMissingTimestampFieldMetadata = (): EcsBasedFieldMetadata => ({ isEcsCompliant: false, isInSameFamily: false, // `date` is not a member of any families }); + +export const getMappingsProperties = ({ + indexes, + indexName, +}: { + indexes: Record | null; + indexName: string; +}): Record | null => { + if (indexes != null && indexes[indexName] != null) { + return indexes[indexName].mappings.properties ?? null; + } + + return null; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.ts index b8f60be24a87c..503edf5c230cf 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/utils/stats.ts @@ -7,6 +7,20 @@ import { DataQualityCheckResult, MeteringStatsIndex, PatternRollup } from '../types'; +export const getIndexIncompatible = ({ + indexName, + results, +}: { + indexName: string; + results: Record | undefined; +}): number | undefined => { + if (results == null || results[indexName] == null) { + return undefined; + } + + return results[indexName].incompatible; +}; + export const getSizeInBytes = ({ indexName, stats, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ff2f7c500b177..9e93abc0a6b86 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6963,7 +6963,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "Réduire", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "Documents", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "Développer les lignes", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "Échoué", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "Phase ILM", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "Champs incompatibles", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "Index", @@ -6972,10 +6971,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "Index vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "Index", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "Dernière vérification", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.passedTooltip": "Approuvé", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "Résultat", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "Taille", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.thisIndexHasNotBeenCheckedTooltip": "Cet index n'a pas été vérifié", "securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "Date/heure d'origine de l'événement. Il s'agit des date et heure extraites de l'événement, représentant généralement le moment auquel l'événement a été généré par la source. Si la source de l'événement ne comporte pas d'horodatage original, cette valeur est habituellement remplie la première fois que l'événement a été reçu par le pipeline. Champs requis pour tous les événements.", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "Erreurs copiées dans le presse-papiers", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "Résultats copiés dans le presse-papiers", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c72e9d555ddf2..9ca9978c7e299 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6959,7 +6959,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "縮小", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "ドキュメント", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "行を展開", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "失敗", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILMフェーズ", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "非互換フィールド", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "インデックス", @@ -6968,10 +6967,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "確認されたインデックス", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "インデックス", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "最終確認", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.passedTooltip": "合格", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "結果", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "サイズ", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.thisIndexHasNotBeenCheckedTooltip": "このインデックスは確認されていません", "securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "イベントが生成された日時これはイベントから抽出された日時で、一般的にはイベントがソースから生成された日時を表します。イベントソースに元のタイムスタンプがない場合は、通常、この値はイベントがパイプラインによって受信された最初の日時が入力されます。すべてのイベントの必須フィールドです。", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "エラーをクリップボードにコピーしました", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "結果をクリップボードにコピーしました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 14c23084fd8c7..cd9ccb59d81bb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6970,7 +6970,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "折叠", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "文档", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "展开行", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.failedTooltip": "失败", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILM 阶段", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "不兼容的字段", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "索引", @@ -6979,10 +6978,8 @@ "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "已检查索引", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "索引", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "上次检查", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.passedTooltip": "通过", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "结果", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "大小", - "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.thisIndexHasNotBeenCheckedTooltip": "尚未检查此索引", "securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "事件发生时的日期/时间。这是从事件中提取的日期/时间,通常表示源生成事件的时间。如果事件源没有原始时间戳,通常会在管道首次收到事件时填充此值。所有事件的必填字段。", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "已将错误复制到剪贴板", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "已将结果复制到剪贴板",