[Security Solution][DQD][Tech Debt] Refactor lower level helpers (#191245)

addresses https://github.com/elastic/kibana/issues/190964

Fourth in the series of PRs to address general DQD tech debt

This one builds on previous 3 PRs

https://github.com/elastic/kibana/pull/190970
https://github.com/elastic/kibana/pull/190978
https://github.com/elastic/kibana/pull/191233

Gist of changes:

split lower level helpers into series of utils/* files
each utils/ file is named after common behavior it export or works with.
cleanup dead code
This commit is contained in:
Karen Grigoryan 2024-08-28 10:12:08 +02:00 committed by GitHub
parent 094f4a3ecd
commit 7f22ca5cf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 3137 additions and 2900 deletions

View file

@ -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<string, EcsFieldMetadata>;
@ -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: [],
};

View file

@ -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;

View file

@ -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<string, IlmExplainLifecycleLifecycleExplain> | 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<string, IlmExplainLifecycleLifecycleExplain> | null
): IlmExplainPhaseCounts => {
const indexNames = ilmExplain != null ? Object.keys(ilmExplain) : [];
return indexNames.reduce<IlmExplainPhaseCounts>(
(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<string, DataQualityCheckResult> | 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<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[];
isILMAvailable: boolean;
pattern: string;
patternDocsCount: number;
results: Record<string, DataQualityCheckResult> | undefined;
sortByColumn: string;
sortByDirection: 'desc' | 'asc';
stats: Record<string, MeteringStatsIndex> | 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<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[] | undefined;
isILMAvailable: boolean;
newIndexNames: string[];
stats: Record<string, MeteringStatsIndex> | 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<string, IlmExplainLifecycleLifecycleExplain> | null;
isILMAvailable: boolean;
newDocsCount: number;
patternRollup: PatternRollup | undefined;
stats: Record<string, MeteringStatsIndex> | 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;
}
};

View file

@ -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[] = [];

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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<string, MappingProperty> | null | undefined;
unallowedValues: Record<string, UnallowedValueCount[]> | 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<string, IndicesGetMappingIndexMappingRecord> | null;
indexName: string;
}): Record<string, MappingProperty> | null => {
if (indexes != null && indexes[indexName] != null) {
return indexes[indexName].mappings.properties ?? null;
}
return null;
};

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 = '--';

View file

@ -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'
);
});
});

View file

@ -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}`;

View file

@ -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)`

View file

@ -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');
});
});

View file

@ -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';
}
};

View file

@ -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);
});
});
});

View file

@ -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',

View file

@ -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;

View file

@ -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);
});
});
});

View file

@ -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;
}
};

View file

@ -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);
});
});

View file

@ -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<T> {
incompatible: T;
indices: T;

View file

@ -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',

View file

@ -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: ({

View file

@ -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',
{

View file

@ -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;

View file

@ -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<EuiBasicTableColumn<IndexSummaryTableItem>> =>
@ -247,11 +221,3 @@ export const getSummaryTableColumns = ({
width: '120px',
},
];
export const getShowPagination = ({
minPageSize,
totalItemCount,
}: {
minPageSize: number;
totalItemCount: number;
}): boolean => totalItemCount > minPageSize;

View file

@ -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);
});
});

View file

@ -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;

View file

@ -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',
}
);

View file

@ -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', () => {

View file

@ -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) {

View file

@ -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();
});
});
});

View file

@ -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;
}
};

View file

@ -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<string, IlmExplainLifecycleLifecycleExplainManaged> = {
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<string, IlmExplainLifecycleLifecycleExplain> = {
...managed,
[unmanaged.index]: unmanaged,
};
expect(getIlmExplainPhaseCounts(ilmExplain)).toEqual({
cold: 1,
frozen: 1,
hot: 1,
unmanaged: 1,
warm: 1,
});
});
});

View file

@ -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<string, IlmExplainLifecycleLifecycleExplain> | 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<string, IlmExplainLifecycleLifecycleExplain> | null
): IlmExplainPhaseCounts => {
const indexNames = ilmExplain != null ? Object.keys(ilmExplain) : [];
return indexNames.reduce<IlmExplainPhaseCounts>(
(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,
}
);
};

View file

@ -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);
});
});

View file

@ -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<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[] | undefined;
isILMAvailable: boolean;
newIndexNames: string[];
stats: Record<string, MeteringStatsIndex> | null;
}): boolean => {
return (
!isEqual(newIndexNames, indexNames) &&
stats != null &&
((isILMAvailable && ilmExplain != null) || !isILMAvailable)
);
};

View file

@ -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);
});
});

View file

@ -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<string, IlmExplainLifecycleLifecycleExplain> | null;
isILMAvailable: boolean;
newDocsCount: number;
patternRollup: PatternRollup | undefined;
stats: Record<string, MeteringStatsIndex> | 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;
};

View file

@ -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%');
});
});

View file

@ -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,
})
: '';

View file

@ -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<string, PatternRollup> = {
'.alerts-security.alerts-default': alertIndexWithAllResults,
'auditbeat-*': auditbeatWithAllResults,
'packetbeat-*': packetbeatNoResults,
};
/** a valid `PatternRollup` that has an undefined `sizeInBytes` */
const noSizeInBytes: Record<string, PatternRollup> = {
'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'));
});
});
});

View file

@ -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<string, PatternRollup>;
}): number | undefined => {
if (patternRollups[pattern] != null) {
return patternRollups[pattern].sizeInBytes;
} else {
return undefined;
}
};
export const getPatternDocsCount = ({
pattern,
patternRollups,
}: {
pattern: string;
patternRollups: Record<string, PatternRollup>;
}): number => {
if (patternRollups[pattern] != null) {
return patternRollups[pattern].docsCount ?? 0;
} else {
return 0;
}
};
export const getPatternLegendItem = ({
pattern,
patternRollups,
}: {
pattern: string;
patternRollups: Record<string, PatternRollup>;
}): 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<string, PatternRollup>;
}): LegendItem[] =>
patterns.reduce<LegendItem[]>(
(acc, pattern) => [
...acc,
getPatternLegendItem({ pattern, patternRollups }),
...getLegendItemsForPattern({ pattern, flattenedBuckets }),
],
[]
);
export const getFlattenedBuckets = ({
ilmPhases,
isILMAvailable,
patternRollups,
}: {
ilmPhases: string[];
isILMAvailable: boolean;
patternRollups: Record<string, PatternRollup>;
}): FlattenedBucket[] =>
Object.values(patternRollups).reduce<FlattenedBucket[]>((acc, patternRollup) => {
// enables fast lookup of valid phase names:
const ilmPhasesMap = ilmPhases.reduce<Record<string, number>>(
(phasesMap, phase) => ({ ...phasesMap, [phase]: 0 }),
{}
);
const { ilmExplain, pattern, results, stats } = patternRollup;
if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) {
return [
...acc,
...Object.entries(stats).reduce<FlattenedBucket[]>((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<string, FlattenedBucket | undefined> =>
flattenedBuckets.reduce<Record<string, FlattenedBucket | undefined>>(
(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<string, FlattenedBucket | undefined>;
}) => {
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<ArrayNode, 'path'>) => {
const pattern = getGroupFromPath(node.path) ?? '';
const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`];
return getFillColor(flattenedBucket?.incompatible);
},
},
},
];
};

View file

@ -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;

View file

@ -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;

View file

@ -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) =>

View file

@ -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

View file

@ -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);
});
});

View file

@ -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;
}
};

View file

@ -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<string, PatternRollup> = {
'.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'));
});
});

View file

@ -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<string, FlattenedBucket | undefined>;
}) => {
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<ArrayNode, 'path'>) => {
const pattern = getGroupFromPath(node.path) ?? '';
const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`];
return getFillColor(flattenedBucket?.incompatible);
},
},
},
];
};

View file

@ -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<string, PatternRollup> = {
'.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,
},
]);
});
});

View file

@ -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<string, PatternRollup>;
}): 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<string, PatternRollup>;
}): LegendItem[] =>
patterns.reduce<LegendItem[]>(
(acc, pattern) => [
...acc,
getPatternLegendItem({ pattern, patternRollups }),
...getLegendItemsForPattern({ pattern, flattenedBuckets }),
],
[]
);

View file

@ -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<string, PatternRollup> = {
'.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,
},
});
});
});
});

View file

@ -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<string, FlattenedBucket | undefined> =>
flattenedBuckets.reduce<Record<string, FlattenedBucket | undefined>>(
(acc, { pattern, indexName, ...remaining }) => ({
...acc,
[`${pattern}${indexName}`]: { pattern, indexName, ...remaining },
}),
{}
);

View file

@ -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<string, PatternRollup> = {
'.alerts-security.alerts-default': alertIndexWithAllResults,
'auditbeat-*': auditbeatWithAllResults,
'packetbeat-*': packetbeatNoResults,
};
/** a valid `PatternRollup` that has an undefined `sizeInBytes` */
const noSizeInBytes: Record<string, PatternRollup> = {
'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();
});
});

View file

@ -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<string, PatternRollup>;
}): number | undefined => {
if (patternRollups[pattern] != null) {
return patternRollups[pattern].sizeInBytes;
} else {
return undefined;
}
};
export const getPatternDocsCount = ({
pattern,
patternRollups,
}: {
pattern: string;
patternRollups: Record<string, PatternRollup>;
}): number => {
if (patternRollups[pattern] != null) {
return patternRollups[pattern].docsCount ?? 0;
} else {
return 0;
}
};

View file

@ -5,15 +5,20 @@
* 2.0.
*/
import { IlmPhase } from '../../../types';
export interface IndexSummaryTableItem {
docsCount: number;
incompatible: number | undefined;
indexName: string;
ilmPhase: IlmPhase | undefined;
export interface LegendItem {
color: string | null;
ilmPhase: string | null;
index: string | null;
pattern: string;
patternDocsCount: number;
sizeInBytes: number | undefined;
checkedAt: number | undefined;
docsCount: number;
}
export interface FlattenedBucket {
ilmPhase: string | undefined;
incompatible: number | undefined;
indexName: string | undefined;
pattern: string;
sizeInBytes: number | undefined;
docsCount: number;
}

View file

@ -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<string, PatternRollup> = {
'.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,
},
]);
});
});

View file

@ -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<string, PatternRollup>;
}): FlattenedBucket[] =>
Object.values(patternRollups).reduce<FlattenedBucket[]>((acc, patternRollup) => {
// enables fast lookup of valid phase names:
const ilmPhasesMap = ilmPhases.reduce<Record<string, number>>(
(phasesMap, phase) => ({ ...phasesMap, [phase]: 0 }),
{}
);
const { ilmExplain, pattern, results, stats } = patternRollup;
if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) {
return [
...acc,
...Object.entries(stats).reduce<FlattenedBucket[]>((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;
}, []);

View file

@ -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<string, string[]> = {
'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);
});
});
});

View file

@ -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';

View file

@ -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}',
}
);

View file

@ -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<string, string[]> = {
'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-*',
},
]);
});
});

View file

@ -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<string, MeteringStatsIndex> | null = patternRollup?.stats ?? null;
return getDocsCount({
indexName,
stats,
});
};

View file

@ -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;

View file

@ -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[];

View file

@ -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,

View file

@ -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);
});
});

View file

@ -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<string, DataQualityCheckResult> | undefined
@ -98,3 +102,18 @@ export const getIndexId = ({
indexName: string;
stats: Record<string, MeteringStatsIndex> | null;
}): string | null | undefined => stats && stats[indexName]?.uuid;
export const getIndexDocsCountFromRollup = ({
indexName,
patternRollup,
}: {
indexName: string;
patternRollup: PatternRollup;
}): number => {
const stats: Record<string, MeteringStatsIndex> | null = patternRollup?.stats ?? null;
return getDocsCount({
indexName,
stats,
});
};

View file

@ -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,

View file

@ -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}',
}
);

View file

@ -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;
}

View file

@ -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 }) =>

View file

@ -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: [],

View file

@ -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<string, IlmExplainLifecycleLifecycleExplainManaged> = {
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');
});
});
});

View file

@ -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';
}
};

View file

@ -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<string, DataQualityCheckResult> = {
'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,
},
]);
});
});

View file

@ -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<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[];
isILMAvailable: boolean;
pattern: string;
patternDocsCount: number;
results: Record<string, DataQualityCheckResult> | undefined;
sortByColumn: string;
sortByDirection: 'desc' | 'asc';
stats: Record<string, MeteringStatsIndex> | 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);
};

View file

@ -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();
});
});

View file

@ -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<string, MappingProperty> | null | undefined;
unallowedValues: Record<string, UnallowedValueCount[]> | 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<string, IndicesGetMappingIndexMappingRecord> | null;
indexName: string;
}): Record<string, MappingProperty> | null => {
if (indexes != null && indexes[indexName] != null) {
return indexes[indexName].mappings.properties ?? null;
}
return null;
};

View file

@ -7,6 +7,20 @@
import { DataQualityCheckResult, MeteringStatsIndex, PatternRollup } from '../types';
export const getIndexIncompatible = ({
indexName,
results,
}: {
indexName: string;
results: Record<string, DataQualityCheckResult> | undefined;
}): number | undefined => {
if (results == null || results[indexName] == null) {
return undefined;
}
return results[indexName].incompatible;
};
export const getSizeInBytes = ({
indexName,
stats,

View file

@ -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",

View file

@ -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": "結果をクリップボードにコピーしました",

View file

@ -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": "已将结果复制到剪贴板",