[Security Solution][DQD][Tech Debt] Dissolve index properties markdown (#191264)

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

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

This one builds on previous 4 PRs

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

Gist of changes:

- split gigantic markdown helper file and colocate the parts where they
belong
- dedupe translations
- cleanup dead code
This commit is contained in:
Karen Grigoryan 2024-08-28 14:25:49 +02:00 committed by GitHub
parent 096c52f096
commit f79b714fda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1454 additions and 1525 deletions

View file

@ -57,3 +57,13 @@ export const EMPTY_METADATA: PartitionedFieldMetadata = {
incompatible: [],
sameFamily: [],
};
export const EMPTY_PLACEHOLDER = '--';
export const ECS_FIELD_REFERENCE_URL =
'https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html';
/** The documentation link shown in the `Data Quality` dashboard */
export const ECS_REFERENCE_URL = 'https://www.elastic.co/guide/en/ecs/current/ecs-reference.html';
export const MAPPING_URL =
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html';

View file

@ -15,6 +15,11 @@ import {
HOT_DESCRIPTION,
UNMANAGED_DESCRIPTION,
WARM_DESCRIPTION,
HOT,
WARM,
FROZEN,
COLD,
UNMANAGED,
} from '../../translations';
import * as i18n from './translations';
@ -41,17 +46,17 @@ const IlmPhasesEmptyPromptComponent: React.FC = () => {
<Ul>
<Li>
<strong>{i18n.HOT}</strong>
<strong>{HOT}</strong>
{': '}
{HOT_DESCRIPTION}
</Li>
<Li>
<strong>{i18n.WARM}</strong>
<strong>{WARM}</strong>
{': '}
{WARM_DESCRIPTION}
</Li>
<Li>
<strong>{i18n.UNMANAGED}</strong>
<strong>{UNMANAGED}</strong>
{': '}
{UNMANAGED_DESCRIPTION}
</Li>
@ -71,12 +76,12 @@ const IlmPhasesEmptyPromptComponent: React.FC = () => {
<Ul>
<Li>
<strong>{i18n.COLD}</strong>
<strong>{COLD}</strong>
{': '}
{COLD_DESCRIPTION}
</Li>
<Li>
<strong>{i18n.FROZEN}</strong>
<strong>{FROZEN}</strong>
{': '}
{FROZEN_DESCRIPTION}
</Li>

View file

@ -15,27 +15,6 @@ export const BODY = i18n.translate(
}
);
export const COLD = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel',
{
defaultMessage: 'cold',
}
);
export const FROZEN = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptFrozenLabel',
{
defaultMessage: 'frozen',
}
);
export const HOT = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptHotLabel',
{
defaultMessage: 'hot',
}
);
export const ILM_PHASES_THAT_CAN_BE_CHECKED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCanBeCheckedSubtitle',
{
@ -58,20 +37,6 @@ export const THE_FOLLOWING_ILM_PHASES = i18n.translate(
}
);
export const UNMANAGED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel',
{
defaultMessage: 'unmanaged',
}
);
export const WARM = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel',
{
defaultMessage: 'warm',
}
);
export const TITLE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle',
{

View file

@ -8,7 +8,7 @@
import { EuiCallOut, EuiCode } from '@elastic/eui';
import React from 'react';
import * as i18n from '../../../../data_quality_summary/summary_actions/check_status/errors_popover/translations';
import * as i18n from '../../../../translations';
interface Props {
title: string;

View file

@ -9,7 +9,6 @@ import { render, screen } from '@testing-library/react';
import { omit } from 'lodash/fp';
import React from 'react';
import { SAME_FAMILY } from '../same_family/translations';
import {
eventCategory,
someField,
@ -26,6 +25,7 @@ import {
} from '../translations';
import { EnrichedFieldMetadata } from '../../../../../../../../../types';
import { EMPTY_PLACEHOLDER, getCommonTableColumns } from '.';
import { SAME_FAMILY_BADGE_LABEL } from '../../../translate';
describe('getCommonTableColumns', () => {
test('it returns the expected column configuration', () => {
@ -128,7 +128,7 @@ describe('getCommonTableColumns', () => {
});
test('it renders the same family badge', () => {
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY);
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY_BADGE_LABEL);
});
});

View file

@ -9,11 +9,11 @@ import { render, screen } from '@testing-library/react';
import { omit } from 'lodash/fp';
import React from 'react';
import { SAME_FAMILY } from '../same_family/translations';
import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers';
import { eventCategory } from '../../../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata';
import { EcsBasedFieldMetadata } from '../../../../../../../../../types';
import { getIncompatibleMappingsTableColumns } from '.';
import { SAME_FAMILY_BADGE_LABEL } from '../../../translate';
describe('getIncompatibleMappingsTableColumns', () => {
test('it returns the expected column configuration', () => {
@ -97,7 +97,7 @@ describe('getIncompatibleMappingsTableColumns', () => {
});
test('it renders the same family badge', () => {
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY);
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY_BADGE_LABEL);
});
});

View file

@ -8,9 +8,9 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { SAME_FAMILY } from './translations';
import { TestExternalProviders } from '../../../../../../../../../mock/test_providers/test_providers';
import { SameFamily } from '.';
import { SAME_FAMILY_BADGE_LABEL } from '../../../translate';
describe('SameFamily', () => {
test('it renders a badge with the expected content', () => {
@ -20,6 +20,6 @@ describe('SameFamily', () => {
</TestExternalProviders>
);
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY);
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY_BADGE_LABEL);
});
});

View file

@ -8,16 +8,15 @@
import { EuiBadge } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { SAME_FAMILY_BADGE_LABEL } from '../../../translate';
const SameFamilyBadge = styled(EuiBadge)`
margin: ${({ theme }) => `0 ${theme.eui.euiSizeXS}`};
`;
import * as i18n from './translations';
const SameFamilyComponent: React.FC = () => (
<SameFamilyBadge data-test-subj="sameFamily" color="warning">
{i18n.SAME_FAMILY}
{SAME_FAMILY_BADGE_LABEL}
</SameFamilyBadge>
);

View file

@ -15,7 +15,7 @@ import {
getMarkdownTable,
getTabCountsMarkdownComment,
getSummaryTableMarkdownComment,
} from '../../../markdown/helpers';
} from '../../utils/markdown';
import * as i18n from '../../../translations';
import type {
CustomFieldMetadata,

View file

@ -9,6 +9,7 @@ import { EuiBadge } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { INCOMPATIBLE_FIELDS } from '../../../../../../../translations';
import { getSizeInBytes } from '../../../../../../../utils/stats';
import { getIncompatibleStatBadgeColor } from '../../../../../../../utils/get_incompatible_stat_badge_color';
import { AllTab } from './all_tab';
@ -22,7 +23,6 @@ import {
INCOMPATIBLE_TAB_ID,
SAME_FAMILY_TAB_ID,
} from '../constants';
import { getMarkdownComment } from '../../markdown/helpers';
import * as i18n from '../../translations';
import { SameFamilyTab } from './same_family_tab';
import type {
@ -31,6 +31,7 @@ import type {
MeteringStatsIndex,
PartitionedFieldMetadata,
} from '../../../../../../../types';
import { getMarkdownComment } from '../utils/markdown';
export const getMissingTimestampComment = (): string =>
getMarkdownComment({
@ -96,7 +97,7 @@ export const getTabs = ({
/>
),
id: INCOMPATIBLE_TAB_ID,
name: i18n.INCOMPATIBLE_FIELDS,
name: INCOMPATIBLE_FIELDS,
},
{
append: <StyledBadge color="hollow">{partitionedFieldMetadata.sameFamily.length}</StyledBadge>,

View file

@ -7,16 +7,16 @@
import { EcsVersion } from '@elastic/ecs';
import { escapeNewlines } from '../../../../../../../../utils/markdown';
import {
getSummaryMarkdownComment,
getIncompatibleMappingsMarkdownTableRows,
getIncompatibleValuesMarkdownTableRows,
getMarkdownComment,
getMarkdownTable,
getSummaryTableMarkdownComment,
getTabCountsMarkdownComment,
escape,
} from '../../../markdown/helpers';
getSummaryTableMarkdownComment,
} from '../../utils/markdown';
import * as i18n from '../../../translations';
import type {
EcsBasedFieldMetadata,
@ -69,7 +69,7 @@ export const getIncompatibleMappingsFields = (
x.type !== x.indexFieldType &&
!getIsInSameFamily({ ecsExpectedType: x.type, type: x.indexFieldType })
) {
const field = escape(x.indexFieldName);
const field = escapeNewlines(x.indexFieldName);
if (field != null) {
return [...acc, field];
}
@ -80,7 +80,7 @@ export const getIncompatibleMappingsFields = (
export const getSameFamilyFields = (ecsBasedFieldMetadata: EcsBasedFieldMetadata[]): string[] =>
ecsBasedFieldMetadata.reduce<string[]>((acc, x) => {
if (!x.isEcsCompliant && x.type !== x.indexFieldType && x.isInSameFamily) {
const field = escape(x.indexFieldName);
const field = escapeNewlines(x.indexFieldName);
if (field != null) {
return [...acc, field];
}
@ -98,7 +98,7 @@ export const getIncompatibleValuesFields = (
): string[] =>
ecsBasedFieldMetadata.reduce<string[]>((acc, x) => {
if (!x.isEcsCompliant && x.indexInvalidValues.length > 0) {
const field = escape(x.indexFieldName);
const field = escapeNewlines(x.indexFieldName);
if (field != null) {
return [...acc, field];
}

View file

@ -17,9 +17,9 @@ import {
getIncompatibleMappingsMarkdownTableRows,
getMarkdownComment,
getMarkdownTable,
getSummaryTableMarkdownComment,
getTabCountsMarkdownComment,
} from '../../../markdown/helpers';
getSummaryTableMarkdownComment,
} from '../../utils/markdown';
import * as i18n from '../../../translations';
import { SAME_FAMILY_FIELD_MAPPINGS_TABLE_TITLE } from './translations';
import type {

View file

@ -0,0 +1,336 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import numeral from '@elastic/numeral';
import {
eventCategory,
mockCustomFields,
mockIncompatibleMappings,
sourceIpWithTextMapping,
} from '../../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata';
import { EMPTY_STAT } from '../../../../../../../constants';
import { mockPartitionedFieldMetadata } from '../../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata';
import {
escapePreserveNewlines,
getAllowedValues,
getCustomMarkdownTableRows,
getIncompatibleMappingsMarkdownTableRows,
getIncompatibleValuesMarkdownTableRows,
getIndexInvalidValues,
getMarkdownComment,
getMarkdownTable,
getSameFamilyBadge,
getSummaryMarkdownComment,
getSummaryTableMarkdownComment,
getTabCountsMarkdownComment,
} from './markdown';
import {
ECS_MAPPING_TYPE_EXPECTED,
FIELD,
INDEX_MAPPING_TYPE_ACTUAL,
} from '../tabs/compare_fields_table/translations';
import { mockAllowedValues } from '../../../../../../../mock/allowed_values/mock_allowed_values';
import { EcsBasedFieldMetadata, UnallowedValueCount } from '../../../../../../../types';
import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../tabs/incompatible_tab/translations';
import { escapeNewlines } from '../../../../../../../utils/markdown';
import { SAME_FAMILY_BADGE_LABEL } from '../translate';
const defaultBytesFormat = '0,0.[0]b';
const formatBytes = (value: number | undefined) =>
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
const defaultNumberFormat = '0,0.[000]';
const formatNumber = (value: number | undefined) =>
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
const indexName = 'auditbeat-custom-index-1';
describe('getSummaryTableMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n'
);
});
test('it returns the expected comment when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n'
);
});
test('it returns the expected comment when sizeInBytes is undefined', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n'
);
});
});
describe('escapeNewlines', () => {
test('it returns undefined when `content` is undefined', () => {
expect(escapeNewlines(undefined)).toBeUndefined();
});
test("it returns the content unmodified when there's nothing to escape", () => {
const content = "there's nothing to escape in this content";
expect(escapeNewlines(content)).toEqual(content);
});
test('it replaces all newlines in the content with spaces', () => {
const content = '\nthere were newlines in the beginning, middle,\nand end\n';
expect(escapeNewlines(content)).toEqual(
' there were newlines in the beginning, middle, and end '
);
});
test('it escapes all column separators in the content with spaces', () => {
const content = '|there were column separators in the beginning, middle,|and end|';
expect(escapeNewlines(content)).toEqual(
'\\|there were column separators in the beginning, middle,\\|and end\\|'
);
});
test('it escapes content containing BOTH newlines and column separators', () => {
const content =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(escapeNewlines(content)).toEqual(
'\\| there were newlines and column separators in the beginning, middle, \\|and end\\| '
);
});
});
describe('escapePreserveNewlines', () => {
test('it returns undefined when `content` is undefined', () => {
expect(escapePreserveNewlines(undefined)).toBeUndefined();
});
test("it returns the content unmodified when there's nothing to escape", () => {
const content = "there's (also) nothing to escape in this content";
expect(escapePreserveNewlines(content)).toEqual(content);
});
test('it escapes all column separators in the content with spaces', () => {
const content = '|there were column separators in the beginning, middle,|and end|';
expect(escapePreserveNewlines(content)).toEqual(
'\\|there were column separators in the beginning, middle,\\|and end\\|'
);
});
test('it does NOT escape newlines in the content', () => {
const content =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(escapePreserveNewlines(content)).toEqual(
'\\|\nthere were newlines and column separators in the beginning, middle,\n\\|and end\\|\n'
);
});
});
describe('getAllowedValues', () => {
test('it returns the expected placeholder when `allowedValues` is undefined', () => {
expect(getAllowedValues(undefined)).toEqual('`--`');
});
test('it joins the `allowedValues` `name`s as a markdown-code-formatted, comma separated, string', () => {
expect(getAllowedValues(mockAllowedValues)).toEqual(
'`authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web`'
);
});
});
describe('getIndexInvalidValues', () => {
test('it returns the expected placeholder when `indexInvalidValues` is empty', () => {
expect(getIndexInvalidValues([])).toEqual('`--`');
});
test('it returns markdown-code-formatted `fieldName`s, and their associated `count`s', () => {
const indexInvalidValues: UnallowedValueCount[] = [
{
count: 2,
fieldName: 'an_invalid_category',
},
{
count: 1,
fieldName: 'theory',
},
];
expect(getIndexInvalidValues(indexInvalidValues)).toEqual(
`\`an_invalid_category\` (2), \`theory\` (1)`
);
});
});
describe('getCustomMarkdownTableRows', () => {
test('it returns the expected table rows', () => {
expect(getCustomMarkdownTableRows(mockCustomFields)).toEqual(
'| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |'
);
});
});
describe('getSameFamilyBadge', () => {
test('it returns the expected badge text when the field is in the same family', () => {
const inSameFamily = {
...eventCategory,
isInSameFamily: true,
};
expect(getSameFamilyBadge(inSameFamily)).toEqual(`\`${SAME_FAMILY_BADGE_LABEL}\``);
});
test('it returns an empty string when the field is NOT the same family', () => {
const notInSameFamily = {
...eventCategory,
isInSameFamily: false,
};
expect(getSameFamilyBadge(notInSameFamily)).toEqual('');
});
});
describe('getIncompatibleMappingsMarkdownTableRows', () => {
test('it returns the expected table rows when the field is in the same family', () => {
const eventCategoryWithWildcard: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType: 'wildcard', // this index has a mapping of `wildcard` instead of `keyword`
isInSameFamily: true, // `wildcard` and `keyword` are in the same family
};
expect(
getIncompatibleMappingsMarkdownTableRows([eventCategoryWithWildcard, sourceIpWithTextMapping])
).toEqual(
'| event.category | `keyword` | `wildcard` `same family` |\n| source.ip | `ip` | `text` |'
);
});
test('it returns the expected table rows when the field is NOT in the same family', () => {
const eventCategoryWithText: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType: 'text', // this index has a mapping of `text` instead of `keyword`
isInSameFamily: false, // `text` and `keyword` are NOT in the same family
};
expect(
getIncompatibleMappingsMarkdownTableRows([eventCategoryWithText, sourceIpWithTextMapping])
).toEqual('| event.category | `keyword` | `text` |\n| source.ip | `ip` | `text` |');
});
});
describe('getIncompatibleValuesMarkdownTableRows', () => {
test('it returns the expected table rows', () => {
expect(
getIncompatibleValuesMarkdownTableRows([
{
...eventCategory,
hasEcsMetadata: true,
indexInvalidValues: [
{
count: 2,
fieldName: 'an_invalid_category',
},
{
count: 1,
fieldName: 'theory',
},
],
isEcsCompliant: false,
},
])
).toEqual(
'| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |'
);
});
});
describe('getMarkdownComment', () => {
test('it returns the expected markdown comment', () => {
const suggestedAction =
'|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n|and end|\n';
const title =
'|\nthere were newlines and column separators in this title beginning, middle,\n|and end|\n';
expect(getMarkdownComment({ suggestedAction, title })).toEqual(
'#### \\| there were newlines and column separators in this title beginning, middle, \\|and end\\| \n\n\\|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n\\|and end\\|\n'
);
});
});
describe('getMarkdownTable', () => {
test('it returns the expected table contents', () => {
expect(
getMarkdownTable({
enrichedFieldMetadata: mockIncompatibleMappings,
getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows,
headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL],
title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName),
})
).toEqual(
'#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n'
);
});
test('it returns an empty string when `enrichedFieldMetadata` is empty', () => {
expect(
getMarkdownTable({
enrichedFieldMetadata: [], // <-- empty
getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows,
headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL],
title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName),
})
).toEqual('');
});
});
describe('getSummaryMarkdownComment', () => {
test('it returns the expected markdown comment', () => {
expect(getSummaryMarkdownComment(indexName)).toEqual('### auditbeat-custom-index-1\n');
});
});
describe('getTabCountsMarkdownComment', () => {
test('it returns a comment with the expected counts', () => {
expect(getTabCountsMarkdownComment(mockPartitionedFieldMetadata)).toBe(
'### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n'
);
});
});

View file

@ -0,0 +1,163 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { INCOMPATIBLE_FIELDS } from '../../../../../../../translations';
import {
escapeNewlines,
getCodeFormattedValue,
getMarkdownTableHeader,
getSummaryTableMarkdownHeader,
getSummaryTableMarkdownRow,
} from '../../../../../../../utils/markdown';
import {
AllowedValue,
CustomFieldMetadata,
EcsBasedFieldMetadata,
EnrichedFieldMetadata,
IlmPhase,
PartitionedFieldMetadata,
UnallowedValueCount,
} from '../../../../../../../types';
import { ALL_FIELDS, CUSTOM_FIELDS, ECS_COMPLIANT_FIELDS, SAME_FAMILY } from '../../translations';
import { SAME_FAMILY_BADGE_LABEL } from '../translate';
export const getSummaryTableMarkdownComment = ({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
`${getSummaryTableMarkdownHeader(isILMAvailable)}
${getSummaryTableMarkdownRow({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
incompatible: partitionedFieldMetadata.incompatible.length,
patternDocsCount,
sizeInBytes,
})}
`;
export const escapePreserveNewlines = (content: string | undefined): string | undefined =>
content != null ? content.replaceAll('|', '\\|') : content;
export const getAllowedValues = (allowedValues: AllowedValue[] | undefined): string =>
allowedValues == null
? getCodeFormattedValue(undefined)
: allowedValues.map((x) => getCodeFormattedValue(x.name)).join(', ');
export const getIndexInvalidValues = (indexInvalidValues: UnallowedValueCount[]): string =>
indexInvalidValues.length === 0
? getCodeFormattedValue(undefined)
: indexInvalidValues
.map(
({ fieldName, count }) => `${getCodeFormattedValue(escapeNewlines(fieldName))} (${count})`
)
.join(', '); // newlines are instead joined with spaces
export const getCustomMarkdownTableRows = (customFieldMetadata: CustomFieldMetadata[]): string =>
customFieldMetadata
.map(
(x) =>
`| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue(
x.indexFieldType
)} | ${getAllowedValues(undefined)} |`
)
.join('\n');
export const getSameFamilyBadge = (ecsBasedFieldMetadata: EcsBasedFieldMetadata): string =>
ecsBasedFieldMetadata.isInSameFamily ? getCodeFormattedValue(SAME_FAMILY_BADGE_LABEL) : '';
export const getIncompatibleMappingsMarkdownTableRows = (
incompatibleMappings: EcsBasedFieldMetadata[]
): string =>
incompatibleMappings
.map(
(x) =>
`| ${escapeNewlines(x.indexFieldName)} | ${getCodeFormattedValue(
x.type
)} | ${getCodeFormattedValue(x.indexFieldType)} ${getSameFamilyBadge(x)} |`
)
.join('\n');
export const getIncompatibleValuesMarkdownTableRows = (
incompatibleValues: EcsBasedFieldMetadata[]
): string =>
incompatibleValues
.map(
(x) =>
`| ${escapeNewlines(x.indexFieldName)} | ${getAllowedValues(
x.allowed_values
)} | ${getIndexInvalidValues(x.indexInvalidValues)} |`
)
.join('\n');
export const getMarkdownComment = ({
suggestedAction,
title,
}: {
suggestedAction: string;
title: string;
}): string =>
`#### ${escapeNewlines(title)}
${escapePreserveNewlines(suggestedAction)}`;
export const getMarkdownTable = <T extends EnrichedFieldMetadata[]>({
enrichedFieldMetadata,
getMarkdownTableRows,
headerNames,
title,
}: {
enrichedFieldMetadata: T;
getMarkdownTableRows: (enrichedFieldMetadata: T) => string;
headerNames: string[];
title: string;
}): string =>
enrichedFieldMetadata.length > 0
? `#### ${escapeNewlines(title)}
${getMarkdownTableHeader(headerNames)}
${getMarkdownTableRows(enrichedFieldMetadata)}
`
: '';
export const getSummaryMarkdownComment = (indexName: string) =>
`### ${escapeNewlines(indexName)}
`;
export const getTabCountsMarkdownComment = (
partitionedFieldMetadata: PartitionedFieldMetadata
): string =>
`### **${INCOMPATIBLE_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.incompatible.length}`
)} **${SAME_FAMILY}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.sameFamily.length}`
)} **${CUSTOM_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.custom.length}`
)} **${ECS_COMPLIANT_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.ecsCompliant.length}`
)} **${ALL_FIELDS}** ${getCodeFormattedValue(`${partitionedFieldMetadata.all.length}`)}
`;

View file

@ -9,9 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { DOCS } from '../translations';
import { ILM_PHASE } from '../../../../../../translations';
import { SIZE } from '../../../summary_table/translations';
import { DOCS, ILM_PHASE, SIZE } from '../../../../../../translations';
import { Stat } from '../../../../../../stat';
import { getIlmPhaseDescription } from '../../../../../../utils/get_ilm_phase_description';

View file

@ -1,727 +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 {
ECS_MAPPING_TYPE_EXPECTED,
FIELD,
INDEX_MAPPING_TYPE_ACTUAL,
} from '../index_check_fields/tabs/compare_fields_table/translations';
import { ERRORS } from '../../../../../../data_quality_summary/summary_actions/check_status/errors_popover/translations';
import {
ERROR,
INDEX,
PATTERN,
} from '../../../../../../data_quality_summary/summary_actions/check_status/errors_popover/errors_viewer/translations';
import {
escape,
escapePreserveNewlines,
getAllowedValues,
getCodeFormattedValue,
getCustomMarkdownTableRows,
getDataQualitySummaryMarkdownComment,
getErrorsMarkdownTable,
getErrorsMarkdownTableRows,
getHeaderSeparator,
getIlmExplainPhaseCountsMarkdownComment,
getIncompatibleMappingsMarkdownTableRows,
getIncompatibleValuesMarkdownTableRows,
getIndexInvalidValues,
getMarkdownComment,
getMarkdownTable,
getMarkdownTableHeader,
getPatternSummaryMarkdownComment,
getResultEmoji,
getSameFamilyBadge,
getStatsRollupMarkdownComment,
getSummaryMarkdownComment,
getSummaryTableMarkdownComment,
getSummaryTableMarkdownHeader,
getSummaryTableMarkdownRow,
getTabCountsMarkdownComment,
} from './helpers';
import { EMPTY_STAT } from '../../../../../../constants';
import { mockAllowedValues } from '../../../../../../mock/allowed_values/mock_allowed_values';
import {
eventCategory,
mockCustomFields,
mockIncompatibleMappings,
sourceIpWithTextMapping,
} from '../../../../../../mock/enriched_field_metadata/mock_enriched_field_metadata';
import { mockPartitionedFieldMetadata } from '../../../../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata';
import {
auditbeatNoResults,
auditbeatWithAllResults,
} from '../../../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
import { SAME_FAMILY } from '../index_check_fields/tabs/compare_fields_table/same_family/translations';
import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../index_check_fields/tabs/incompatible_tab/translations';
import {
EcsBasedFieldMetadata,
ErrorSummary,
PatternRollup,
UnallowedValueCount,
} from '../../../../../../types';
const errorSummary: ErrorSummary[] = [
{
pattern: '.alerts-security.alerts-default',
indexName: null,
error: 'Error loading stats: Error: Forbidden',
},
{
error:
'Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden',
indexName: 'auditbeat-7.2.1-2023.02.13-000001',
pattern: 'auditbeat-*',
},
];
const indexName = 'auditbeat-custom-index-1';
const defaultBytesFormat = '0,0.[0]b';
const formatBytes = (value: number | undefined) =>
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
const defaultNumberFormat = '0,0.[000]';
const formatNumber = (value: number | undefined) =>
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
describe('helpers', () => {
describe('escape', () => {
test('it returns undefined when `content` is undefined', () => {
expect(escape(undefined)).toBeUndefined();
});
test("it returns the content unmodified when there's nothing to escape", () => {
const content = "there's nothing to escape in this content";
expect(escape(content)).toEqual(content);
});
test('it replaces all newlines in the content with spaces', () => {
const content = '\nthere were newlines in the beginning, middle,\nand end\n';
expect(escape(content)).toEqual(' there were newlines in the beginning, middle, and end ');
});
test('it escapes all column separators in the content with spaces', () => {
const content = '|there were column separators in the beginning, middle,|and end|';
expect(escape(content)).toEqual(
'\\|there were column separators in the beginning, middle,\\|and end\\|'
);
});
test('it escapes content containing BOTH newlines and column separators', () => {
const content =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(escape(content)).toEqual(
'\\| there were newlines and column separators in the beginning, middle, \\|and end\\| '
);
});
});
describe('escapePreserveNewlines', () => {
test('it returns undefined when `content` is undefined', () => {
expect(escapePreserveNewlines(undefined)).toBeUndefined();
});
test("it returns the content unmodified when there's nothing to escape", () => {
const content = "there's (also) nothing to escape in this content";
expect(escapePreserveNewlines(content)).toEqual(content);
});
test('it escapes all column separators in the content with spaces', () => {
const content = '|there were column separators in the beginning, middle,|and end|';
expect(escapePreserveNewlines(content)).toEqual(
'\\|there were column separators in the beginning, middle,\\|and end\\|'
);
});
test('it does NOT escape newlines in the content', () => {
const content =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(escapePreserveNewlines(content)).toEqual(
'\\|\nthere were newlines and column separators in the beginning, middle,\n\\|and end\\|\n'
);
});
});
describe('getHeaderSeparator', () => {
test('it returns a sequence of dashes equal to the length of the header, plus two additional dashes to pad each end of the cntent', () => {
const content = '0123456789'; // content.length === 10
const expected = '------------'; // expected.length === 12
expect(getHeaderSeparator(content)).toEqual(expected);
});
});
describe('getMarkdownTableHeader', () => {
const headerNames = [
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n',
'A second column',
'A third column',
];
test('it returns the expected table header', () => {
expect(getMarkdownTableHeader(headerNames)).toEqual(
'\n| \\| there were newlines and column separators in the beginning, middle, \\|and end\\| | A second column | A third column | \n|----------------------------------------------------------------------------------|-----------------|----------------|'
);
});
});
describe('getCodeFormattedValue', () => {
test('it returns the expected placeholder when `value` is undefined', () => {
expect(getCodeFormattedValue(undefined)).toEqual('`--`');
});
test('it returns the content formatted as markdown code', () => {
const value = 'foozle';
expect(getCodeFormattedValue(value)).toEqual('`foozle`');
});
test('it escapes content such that `value` may be included in a markdown table cell', () => {
const value =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(getCodeFormattedValue(value)).toEqual(
'`\\| there were newlines and column separators in the beginning, middle, \\|and end\\| `'
);
});
});
describe('getAllowedValues', () => {
test('it returns the expected placeholder when `allowedValues` is undefined', () => {
expect(getAllowedValues(undefined)).toEqual('`--`');
});
test('it joins the `allowedValues` `name`s as a markdown-code-formatted, comma separated, string', () => {
expect(getAllowedValues(mockAllowedValues)).toEqual(
'`authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web`'
);
});
});
describe('getIndexInvalidValues', () => {
test('it returns the expected placeholder when `indexInvalidValues` is empty', () => {
expect(getIndexInvalidValues([])).toEqual('`--`');
});
test('it returns markdown-code-formatted `fieldName`s, and their associated `count`s', () => {
const indexInvalidValues: UnallowedValueCount[] = [
{
count: 2,
fieldName: 'an_invalid_category',
},
{
count: 1,
fieldName: 'theory',
},
];
expect(getIndexInvalidValues(indexInvalidValues)).toEqual(
`\`an_invalid_category\` (2), \`theory\` (1)`
);
});
});
describe('getCustomMarkdownTableRows', () => {
test('it returns the expected table rows', () => {
expect(getCustomMarkdownTableRows(mockCustomFields)).toEqual(
'| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |'
);
});
});
describe('getSameFamilyBadge', () => {
test('it returns the expected badge text when the field is in the same family', () => {
const inSameFamily = {
...eventCategory,
isInSameFamily: true,
};
expect(getSameFamilyBadge(inSameFamily)).toEqual(`\`${SAME_FAMILY}\``);
});
test('it returns an empty string when the field is NOT the same family', () => {
const notInSameFamily = {
...eventCategory,
isInSameFamily: false,
};
expect(getSameFamilyBadge(notInSameFamily)).toEqual('');
});
});
describe('getIncompatibleMappingsMarkdownTableRows', () => {
test('it returns the expected table rows when the field is in the same family', () => {
const eventCategoryWithWildcard: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType: 'wildcard', // this index has a mapping of `wildcard` instead of `keyword`
isInSameFamily: true, // `wildcard` and `keyword` are in the same family
};
expect(
getIncompatibleMappingsMarkdownTableRows([
eventCategoryWithWildcard,
sourceIpWithTextMapping,
])
).toEqual(
'| event.category | `keyword` | `wildcard` `same family` |\n| source.ip | `ip` | `text` |'
);
});
test('it returns the expected table rows when the field is NOT in the same family', () => {
const eventCategoryWithText: EcsBasedFieldMetadata = {
...eventCategory, // `event.category` is a `keyword` per the ECS spec
indexFieldType: 'text', // this index has a mapping of `text` instead of `keyword`
isInSameFamily: false, // `text` and `keyword` are NOT in the same family
};
expect(
getIncompatibleMappingsMarkdownTableRows([eventCategoryWithText, sourceIpWithTextMapping])
).toEqual('| event.category | `keyword` | `text` |\n| source.ip | `ip` | `text` |');
});
});
describe('getIncompatibleValuesMarkdownTableRows', () => {
test('it returns the expected table rows', () => {
expect(
getIncompatibleValuesMarkdownTableRows([
{
...eventCategory,
hasEcsMetadata: true,
indexInvalidValues: [
{
count: 2,
fieldName: 'an_invalid_category',
},
{
count: 1,
fieldName: 'theory',
},
],
isEcsCompliant: false,
},
])
).toEqual(
'| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |'
);
});
});
describe('getMarkdownComment', () => {
test('it returns the expected markdown comment', () => {
const suggestedAction =
'|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n|and end|\n';
const title =
'|\nthere were newlines and column separators in this title beginning, middle,\n|and end|\n';
expect(getMarkdownComment({ suggestedAction, title })).toEqual(
'#### \\| there were newlines and column separators in this title beginning, middle, \\|and end\\| \n\n\\|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n\\|and end\\|\n'
);
});
});
describe('getErrorsMarkdownTableRows', () => {
test('it returns the expected markdown table rows', () => {
expect(getErrorsMarkdownTableRows(errorSummary)).toEqual(
'| .alerts-security.alerts-default | -- | `Error loading stats: Error: Forbidden` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | `Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden` |'
);
});
});
describe('getErrorsMarkdownTable', () => {
test('it returns the expected table contents', () => {
expect(
getErrorsMarkdownTable({
errorSummary,
getMarkdownTableRows: getErrorsMarkdownTableRows,
headerNames: [PATTERN, INDEX, ERROR],
title: ERRORS,
})
).toEqual(
`## Errors\n\nSome indices were not checked for Data Quality\n\nErrors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for access\n\nThe following privileges are required to check an index:\n- \`monitor\` or \`manage\`\n- \`view_index_metadata\`\n- \`read\`\n\n\n| Pattern | Index | Error | \n|---------|-------|-------|\n| .alerts-security.alerts-default | -- | \`Error loading stats: Error: Forbidden\` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | \`Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden\` |\n`
);
});
test('it returns an empty string when the error summary is empty', () => {
expect(
getErrorsMarkdownTable({
errorSummary: [], // <-- empty
getMarkdownTableRows: getErrorsMarkdownTableRows,
headerNames: [PATTERN, INDEX, ERROR],
title: ERRORS,
})
).toEqual('');
});
});
describe('getMarkdownTable', () => {
test('it returns the expected table contents', () => {
expect(
getMarkdownTable({
enrichedFieldMetadata: mockIncompatibleMappings,
getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows,
headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL],
title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName),
})
).toEqual(
'#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n'
);
});
test('it returns an empty string when `enrichedFieldMetadata` is empty', () => {
expect(
getMarkdownTable({
enrichedFieldMetadata: [], // <-- empty
getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows,
headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL],
title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName),
})
).toEqual('');
});
});
describe('getSummaryMarkdownComment', () => {
test('it returns the expected markdown comment', () => {
expect(getSummaryMarkdownComment(indexName)).toEqual('### auditbeat-custom-index-1\n');
});
});
describe('getTabCountsMarkdownComment', () => {
test('it returns a comment with the expected counts', () => {
expect(getTabCountsMarkdownComment(mockPartitionedFieldMetadata)).toBe(
'### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n'
);
});
});
describe('getResultEmoji', () => {
test('it returns the expected placeholder when `incompatible` is undefined', () => {
expect(getResultEmoji(undefined)).toEqual('--');
});
test('it returns a ✅ when the incompatible count is zero', () => {
expect(getResultEmoji(0)).toEqual('✅');
});
test('it returns a ❌ when the incompatible count is NOT zero', () => {
expect(getResultEmoji(1)).toEqual('❌');
});
});
describe('getSummaryTableMarkdownHeader', () => {
test('it returns the expected header', () => {
const isILMAvailable = true;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|'
);
});
test('it returns the expected header when isILMAvailable is false', () => {
const isILMAvailable = false;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|'
);
});
test('it returns the expected header when displayDocSize is false', () => {
const isILMAvailable = false;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|'
);
});
});
describe('getSummaryTableMarkdownRow', () => {
test('it returns the expected row when all values are provided', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: 3,
ilmPhase: 'unmanaged',
isILMAvailable: true,
indexName: 'auditbeat-custom-index-1',
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n');
});
test('it returns the expected row when optional values are NOT provided', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- | -- | 27.7KB |\n');
});
test('it returns the expected row when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- |\n');
});
test('it returns the expected row when sizeInBytes is undefined', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- |\n');
});
});
describe('getSummaryTableMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n'
);
});
test('it returns the expected comment when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n'
);
});
test('it returns the expected comment when sizeInBytes is undefined', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 |\n\n'
);
});
});
describe('getStatsRollupMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getStatsRollupMarkdownComment({
docsCount: 57410,
formatBytes,
formatNumber,
incompatible: 3,
indices: 25,
indicesChecked: 1,
sizeInBytes: 28413,
})
).toEqual(
'| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 3 | 1 | 25 | 27.7KB | 57,410 |\n'
);
});
test('it returns the expected comment when optional values are undefined', () => {
expect(
getStatsRollupMarkdownComment({
docsCount: 0,
formatBytes,
formatNumber,
incompatible: undefined,
indices: undefined,
indicesChecked: undefined,
sizeInBytes: undefined,
})
).toEqual(
'| Incompatible fields | Indices checked | Indices | Docs |\n|---------------------|-----------------|---------|------|\n| -- | -- | -- | 0 |\n'
);
});
});
describe('getDataQualitySummaryMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getDataQualitySummaryMarkdownComment({
formatBytes,
formatNumber,
totalDocsCount: 3343719,
totalIncompatible: 4,
totalIndices: 30,
totalIndicesChecked: 2,
sizeInBytes: 4294967296,
})
).toEqual(
'# Data quality\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 2 | 30 | 4GB | 3,343,719 |\n\n'
);
});
test('it returns the expected comment when optional values are undefined', () => {
expect(
getDataQualitySummaryMarkdownComment({
formatBytes,
formatNumber,
totalDocsCount: undefined,
totalIncompatible: undefined,
totalIndices: undefined,
totalIndicesChecked: undefined,
sizeInBytes: undefined,
})
).toEqual(
'# Data quality\n\n| Incompatible fields | Indices checked | Indices | Docs |\n|---------------------|-----------------|---------|------|\n| -- | -- | -- | 0 |\n\n'
);
});
});
describe('getIlmExplainPhaseCountsMarkdownComment', () => {
test('it returns the expected comment when _all_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 99,
warm: 8,
unmanaged: 77,
cold: 6,
frozen: 55,
})
).toEqual('`hot(99)` `warm(8)` `unmanaged(77)` `cold(6)` `frozen(55)`');
});
test('it returns the expected comment when _some_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 9,
warm: 0,
unmanaged: 2,
cold: 1,
frozen: 0,
})
).toEqual('`hot(9)` `unmanaged(2)` `cold(1)`');
});
test('it returns the expected comment when _none_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 0,
warm: 0,
unmanaged: 0,
cold: 0,
frozen: 0,
})
).toEqual('');
});
});
describe('getPatternSummaryMarkdownComment', () => {
test('it returns the expected comment when the rollup contains results for all of the indices in the pattern', () => {
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: auditbeatWithAllResults,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when the rollup contains no results', () => {
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: auditbeatNoResults,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| -- | 0 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when the rollup does NOT have `ilmExplainPhaseCounts`', () => {
const noIlmExplainPhaseCounts: PatternRollup = {
...auditbeatWithAllResults,
ilmExplainPhaseCounts: undefined, // <--
};
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: noIlmExplainPhaseCounts,
})
).toEqual(
'## auditbeat-*\n\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when `docsCount` is undefined', () => {
const noDocsCount: PatternRollup = {
...auditbeatWithAllResults,
docsCount: undefined, // <--
};
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: noDocsCount,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 0 |\n\n'
);
});
});
});

View file

@ -1,422 +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 {
getTotalPatternIncompatible,
getTotalPatternIndicesChecked,
} from '../../../../../../utils/stats';
import {
ERRORS_MAY_OCCUR,
ERRORS_CALLOUT_SUMMARY,
MANAGE,
MONITOR,
OR,
READ,
THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED,
VIEW_INDEX_METADATA,
} from '../../../../../../data_quality_summary/summary_actions/check_status/errors_popover/translations';
import { EMPTY_STAT } from '../../../../../../constants';
import { SAME_FAMILY } from '../index_check_fields/tabs/compare_fields_table/same_family/translations';
import {
HOT,
WARM,
COLD,
FROZEN,
UNMANAGED,
} from '../../../../../ilm_phases_empty_prompt/translations';
import * as i18n from '../translations';
import type {
AllowedValue,
CustomFieldMetadata,
EcsBasedFieldMetadata,
EnrichedFieldMetadata,
ErrorSummary,
IlmExplainPhaseCounts,
IlmPhase,
PartitionedFieldMetadata,
PatternRollup,
UnallowedValueCount,
} from '../../../../../../types';
import {
DOCS,
ILM_PHASE,
INCOMPATIBLE_FIELDS,
INDEX,
INDICES,
INDICES_CHECKED,
RESULT,
SIZE,
} from '../../../summary_table/translations';
import { DATA_QUALITY_TITLE } from '../../../../../../translations';
import { getDocsCountPercent } from '../../../utils/stats';
export const EMPTY_PLACEHOLDER = '--';
export const TRIPLE_BACKTICKS = '```';
export const ECS_FIELD_REFERENCE_URL =
'https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html';
/** The documentation link shown in the `Data Quality` dashboard */
export const ECS_REFERENCE_URL = 'https://www.elastic.co/guide/en/ecs/current/ecs-reference.html';
export const MAPPING_URL =
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html';
export const escape = (content: string | undefined): string | undefined =>
content != null ? content.replaceAll('\n', ' ').replaceAll('|', '\\|') : content;
export const escapePreserveNewlines = (content: string | undefined): string | undefined =>
content != null ? content.replaceAll('|', '\\|') : content;
export const getHeaderSeparator = (headerText: string): string => '-'.repeat(headerText.length + 2); // 2 extra, for the spaces on both sides of the column name
export const getMarkdownTableHeader = (headerNames: string[]) => `
| ${headerNames.map((name) => `${escape(name)} | `).join('')}
|${headerNames.map((name) => `${getHeaderSeparator(name)}|`).join('')}`;
export const getCodeFormattedValue = (value: string | undefined) =>
`\`${escape(value ?? EMPTY_PLACEHOLDER)}\``;
export const getAllowedValues = (allowedValues: AllowedValue[] | undefined): string =>
allowedValues == null
? getCodeFormattedValue(undefined)
: allowedValues.map((x) => getCodeFormattedValue(x.name)).join(', ');
export const getIndexInvalidValues = (indexInvalidValues: UnallowedValueCount[]): string =>
indexInvalidValues.length === 0
? getCodeFormattedValue(undefined)
: indexInvalidValues
.map(({ fieldName, count }) => `${getCodeFormattedValue(escape(fieldName))} (${count})`)
.join(', '); // newlines are instead joined with spaces
export const getCustomMarkdownTableRows = (customFieldMetadata: CustomFieldMetadata[]): string =>
customFieldMetadata
.map(
(x) =>
`| ${escape(x.indexFieldName)} | ${getCodeFormattedValue(
x.indexFieldType
)} | ${getAllowedValues(undefined)} |`
)
.join('\n');
export const getSameFamilyBadge = (ecsBasedFieldMetadata: EcsBasedFieldMetadata): string =>
ecsBasedFieldMetadata.isInSameFamily ? getCodeFormattedValue(SAME_FAMILY) : '';
export const getIncompatibleMappingsMarkdownTableRows = (
incompatibleMappings: EcsBasedFieldMetadata[]
): string =>
incompatibleMappings
.map(
(x) =>
`| ${escape(x.indexFieldName)} | ${getCodeFormattedValue(x.type)} | ${getCodeFormattedValue(
x.indexFieldType
)} ${getSameFamilyBadge(x)} |`
)
.join('\n');
export const getIncompatibleValuesMarkdownTableRows = (
incompatibleValues: EcsBasedFieldMetadata[]
): string =>
incompatibleValues
.map(
(x) =>
`| ${escape(x.indexFieldName)} | ${getAllowedValues(
x.allowed_values
)} | ${getIndexInvalidValues(x.indexInvalidValues)} |`
)
.join('\n');
export const getMarkdownComment = ({
suggestedAction,
title,
}: {
suggestedAction: string;
title: string;
}): string =>
`#### ${escape(title)}
${escapePreserveNewlines(suggestedAction)}`;
export const getErrorsMarkdownTableRows = (errorSummary: ErrorSummary[]): string =>
errorSummary
.map(
({ pattern, indexName, error }) =>
`| ${escape(pattern)} | ${escape(indexName ?? EMPTY_PLACEHOLDER)} | ${getCodeFormattedValue(
error
)} |`
)
.join('\n');
export const getErrorsMarkdownTable = ({
errorSummary,
getMarkdownTableRows,
headerNames,
title,
}: {
errorSummary: ErrorSummary[];
getMarkdownTableRows: (errorSummary: ErrorSummary[]) => string;
headerNames: string[];
title: string;
}): string =>
errorSummary.length > 0
? `## ${escape(title)}
${ERRORS_CALLOUT_SUMMARY}
${ERRORS_MAY_OCCUR}
${THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED}
- \`${MONITOR}\` ${OR} \`${MANAGE}\`
- \`${VIEW_INDEX_METADATA}\`
- \`${READ}\`
${getMarkdownTableHeader(headerNames)}
${getMarkdownTableRows(errorSummary)}
`
: '';
export const getMarkdownTable = <T extends EnrichedFieldMetadata[]>({
enrichedFieldMetadata,
getMarkdownTableRows,
headerNames,
title,
}: {
enrichedFieldMetadata: T;
getMarkdownTableRows: (enrichedFieldMetadata: T) => string;
headerNames: string[];
title: string;
}): string =>
enrichedFieldMetadata.length > 0
? `#### ${escape(title)}
${getMarkdownTableHeader(headerNames)}
${getMarkdownTableRows(enrichedFieldMetadata)}
`
: '';
export const getSummaryMarkdownComment = (indexName: string) =>
`### ${escape(indexName)}
`;
export const getTabCountsMarkdownComment = (
partitionedFieldMetadata: PartitionedFieldMetadata
): string =>
`### **${i18n.INCOMPATIBLE_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.incompatible.length}`
)} **${i18n.SAME_FAMILY}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.sameFamily.length}`
)} **${i18n.CUSTOM_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.custom.length}`
)} **${i18n.ECS_COMPLIANT_FIELDS}** ${getCodeFormattedValue(
`${partitionedFieldMetadata.ecsCompliant.length}`
)} **${i18n.ALL_FIELDS}** ${getCodeFormattedValue(`${partitionedFieldMetadata.all.length}`)}
`;
export const getResultEmoji = (incompatible: number | undefined): string => {
if (incompatible == null) {
return EMPTY_PLACEHOLDER;
} else {
return incompatible === 0 ? '✅' : '❌';
}
};
export const getSummaryTableMarkdownHeader = (includeDocSize: boolean): string =>
includeDocSize
? `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE} | ${SIZE} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
ILM_PHASE
)}|${getHeaderSeparator(SIZE)}|`
: `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|`;
export const getSummaryTableMarkdownRow = ({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
incompatible,
indexName,
isILMAvailable,
patternDocsCount,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
incompatible: number | undefined;
indexName: string;
isILMAvailable: boolean;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
isILMAvailable && Number.isInteger(sizeInBytes)
? `| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${
ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER
} | ${formatBytes(sizeInBytes)} |
`
: `| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} |
`;
export const getSummaryTableMarkdownComment = ({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
`${getSummaryTableMarkdownHeader(isILMAvailable)}
${getSummaryTableMarkdownRow({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
incompatible: partitionedFieldMetadata.incompatible.length,
patternDocsCount,
sizeInBytes,
})}
`;
export const getStatsRollupMarkdownComment = ({
docsCount,
formatBytes,
formatNumber,
incompatible,
indices,
indicesChecked,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
incompatible: number | undefined;
indices: number | undefined;
indicesChecked: number | undefined;
sizeInBytes: number | undefined;
}): string =>
Number.isInteger(sizeInBytes)
? `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${SIZE} | ${DOCS} |
|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
INDICES_CHECKED
)}|${getHeaderSeparator(INDICES)}|${getHeaderSeparator(SIZE)}|${getHeaderSeparator(DOCS)}|
| ${incompatible ?? EMPTY_STAT} | ${indicesChecked ?? EMPTY_STAT} | ${
indices ?? EMPTY_STAT
} | ${formatBytes(sizeInBytes)} | ${formatNumber(docsCount)} |
`
: `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${DOCS} |
|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
INDICES_CHECKED
)}|${getHeaderSeparator(INDICES)}|${getHeaderSeparator(DOCS)}|
| ${incompatible ?? EMPTY_STAT} | ${indicesChecked ?? EMPTY_STAT} | ${
indices ?? EMPTY_STAT
} | ${formatNumber(docsCount)} |
`;
export const getDataQualitySummaryMarkdownComment = ({
formatBytes,
formatNumber,
totalDocsCount,
totalIncompatible,
totalIndices,
totalIndicesChecked,
sizeInBytes,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
totalDocsCount: number | undefined;
totalIncompatible: number | undefined;
totalIndices: number | undefined;
totalIndicesChecked: number | undefined;
sizeInBytes: number | undefined;
}): string =>
`# ${DATA_QUALITY_TITLE}
${getStatsRollupMarkdownComment({
docsCount: totalDocsCount ?? 0,
formatBytes,
formatNumber,
incompatible: totalIncompatible,
indices: totalIndices,
indicesChecked: totalIndicesChecked,
sizeInBytes,
})}
`;
export const getIlmExplainPhaseCountsMarkdownComment = ({
hot,
warm,
unmanaged,
cold,
frozen,
}: IlmExplainPhaseCounts): string =>
[
hot > 0 ? getCodeFormattedValue(`${HOT}(${hot})`) : '',
warm > 0 ? getCodeFormattedValue(`${WARM}(${warm})`) : '',
unmanaged > 0 ? getCodeFormattedValue(`${UNMANAGED}(${unmanaged})`) : '',
cold > 0 ? getCodeFormattedValue(`${COLD}(${cold})`) : '',
frozen > 0 ? getCodeFormattedValue(`${FROZEN}(${frozen})`) : '',
]
.filter((x) => x !== '') // prevents extra whitespace
.join(' ');
export const getPatternSummaryMarkdownComment = ({
formatBytes,
formatNumber,
patternRollup,
patternRollup: { docsCount, indices, ilmExplainPhaseCounts, pattern, results },
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
patternRollup: PatternRollup;
}): string =>
`## ${escape(pattern)}
${
ilmExplainPhaseCounts != null
? getIlmExplainPhaseCountsMarkdownComment(ilmExplainPhaseCounts)
: ''
}
${getStatsRollupMarkdownComment({
docsCount: docsCount ?? 0,
formatBytes,
formatNumber,
incompatible: getTotalPatternIncompatible(results),
indices,
indicesChecked: getTotalPatternIndicesChecked(patternRollup),
sizeInBytes: patternRollup.sizeInBytes,
})}
`;

View file

@ -110,13 +110,6 @@ export const CUSTOM_DETECTION_ENGINE_RULES_WORK = i18n.translate(
}
);
export const DOCS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.docsLabel',
{
defaultMessage: 'Docs',
}
);
export const ECS_COMPLIANT_FIELDS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantFieldsLabel',
{
@ -333,13 +326,6 @@ export const CUSTOM_EMPTY_TITLE = i18n.translate(
}
);
export const INCOMPATIBLE_FIELDS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleFieldsTab',
{
defaultMessage: 'Incompatible fields',
}
);
export const INCOMPATIBLE_CALLOUT = (version: string) =>
i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCallout',

View file

@ -14,13 +14,6 @@ export const COLLAPSE = i18n.translate(
}
);
export const DOCS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn',
{
defaultMessage: 'Docs',
}
);
export const VIEW_CHECK_DETAILS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.viewCheckDetailsLabel',
{
@ -35,41 +28,6 @@ export const EXPAND_ROWS = i18n.translate(
}
);
export const ILM_PHASE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn',
{
defaultMessage: 'ILM Phase',
}
);
export const INCOMPATIBLE_FIELDS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn',
{
defaultMessage: 'Incompatible fields',
}
);
export const INDICES = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn',
{
defaultMessage: 'Indices',
}
);
export const INDICES_CHECKED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn',
{
defaultMessage: 'Indices checked',
}
);
export const INDEX = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn',
{
defaultMessage: 'Index',
}
);
export const INDEX_NAME_LABEL = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexesNameLabel',
{
@ -83,20 +41,6 @@ export const INDEX_TOOL_TIP = (pattern: string) =>
defaultMessage: 'This index matches the pattern or index name: {pattern}',
});
export const RESULT = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn',
{
defaultMessage: 'Result',
}
);
export const SIZE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn',
{
defaultMessage: 'Size',
}
);
export const LAST_CHECK = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn',
{

View file

@ -18,16 +18,24 @@ import moment from 'moment';
import styled from 'styled-components';
import { euiThemeVars } from '@kbn/ui-theme';
import { getDocsCountPercent } from '../../../../../utils/stats';
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 {
DOCS,
ILM_PHASE_CAPITALIZED,
INCOMPATIBLE_FIELDS,
INDEX,
INDEX_SIZE_TOOLTIP,
RESULT,
SIZE,
} from '../../../../../translations';
import * as i18n from '../translations';
import { UseIndicesCheckCheckState } from '../../../../../hooks/use_indices_check/types';
import { IndexResultBadge } from '../../index_result_badge';
import { Stat } from '../../../../../stat';
import { getDocsCountPercent } from '../../utils/stats';
import { getIndexResultToolTip } from '../../utils/get_index_result_tooltip';
const ProgressContainer = styled.div`
@ -41,7 +49,7 @@ export const getSummaryTableILMPhaseColumn = (
? [
{
field: 'ilmPhase',
name: i18n.ILM_PHASE,
name: ILM_PHASE_CAPITALIZED,
render: (_, { ilmPhase }) =>
ilmPhase != null ? (
<Stat
@ -70,7 +78,7 @@ export const getSummaryTableSizeInBytesColumn = ({
? [
{
field: 'sizeInBytes',
name: i18n.SIZE,
name: SIZE,
render: (_, { sizeInBytes }) =>
Number.isInteger(sizeInBytes) ? (
<EuiToolTip content={INDEX_SIZE_TOOLTIP}>
@ -143,7 +151,7 @@ export const getSummaryTableColumns = ({
},
{
field: 'incompatible',
name: i18n.RESULT,
name: RESULT,
render: (_, { incompatible }) =>
incompatible != null ? (
<IndexResultBadge incompatible={incompatible} data-test-subj="resultBadge" />
@ -158,7 +166,7 @@ export const getSummaryTableColumns = ({
},
{
field: 'indexName',
name: i18n.INDEX,
name: INDEX,
render: (_, { indexName }) => (
<EuiToolTip content={i18n.INDEX_TOOL_TIP(pattern)}>
<span aria-roledescription={i18n.INDEX_NAME_LABEL} data-test-subj="indexName">
@ -171,7 +179,7 @@ export const getSummaryTableColumns = ({
},
{
field: 'docsCount',
name: i18n.DOCS,
name: DOCS,
render: (_, { docsCount, patternDocsCount }) => (
<ProgressContainer>
<EuiProgress
@ -190,8 +198,8 @@ export const getSummaryTableColumns = ({
},
{
field: 'incompatible',
name: i18n.INCOMPATIBLE_FIELDS,
render: (_, { incompatible, indexName }) => (
name: INCOMPATIBLE_FIELDS,
render: (_, { incompatible }) => (
<EuiToolTip content={INCOMPATIBLE_INDEX_TOOL_TIP}>
<EuiText
size="xs"

View file

@ -10,12 +10,7 @@ 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 {
getDocsCountPercent,
getIndexNames,
getPatternDocsCount,
getPatternSizeInBytes,
} from './stats';
import { getIndexNames, getPatternDocsCount, getPatternSizeInBytes } from './stats';
import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types';
describe('getIndexNames', () => {
@ -237,34 +232,3 @@ 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

@ -70,20 +70,3 @@ 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

@ -9,8 +9,9 @@ import type { EuiTableFieldDataColumnType } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
import React from 'react';
import * as i18n from './translations';
import type { ErrorSummary } from '../../../../../types';
import { INDEX } from '../../../../../translations';
import { ERROR, PATTERN } from '../../../translations';
export const EMPTY_PLACEHOLDER = '--';
@ -20,14 +21,14 @@ export const ERRORS_CONTAINER_MIN_WIDTH = 450; // px
export const getErrorsViewerTableColumns = (): Array<EuiTableFieldDataColumnType<ErrorSummary>> => [
{
field: 'pattern',
name: i18n.PATTERN,
name: PATTERN,
sortable: true,
truncateText: false,
width: '25%',
},
{
field: 'indexName',
name: i18n.INDEX,
name: INDEX,
render: (indexName: string | null) =>
indexName != null && indexName !== '' ? (
<span data-test-subj="indexName">{indexName}</span>
@ -40,7 +41,7 @@ export const getErrorsViewerTableColumns = (): Array<EuiTableFieldDataColumnType
},
{
field: 'error',
name: i18n.ERROR,
name: ERROR,
render: (errorText) => <EuiCode data-test-subj="error">{errorText}</EuiCode>,
sortable: false,
truncateText: false,

View file

@ -8,9 +8,10 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { INDEX } from '../../../../../translations';
import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers';
import { ERROR, INDEX, PATTERN } from './translations';
import { ErrorSummary } from '../../../../../types';
import { ERROR, PATTERN } from '../../../translations';
import { ErrorsViewer } from '.';
interface ExpectedColumns {

View file

@ -16,15 +16,24 @@ import {
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import {
COPY_TO_CLIPBOARD,
ERRORS_CALLOUT_SUMMARY,
ERRORS_MAY_OCCUR,
INDEX,
MANAGE,
MONITOR,
OR,
READ,
THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED,
VIEW_INDEX_METADATA,
} from '../../../../translations';
import { ErrorsViewer } from './errors_viewer';
import { ERRORS_CONTAINER_MAX_WIDTH } from './errors_viewer/helpers';
import {
getErrorsMarkdownTable,
getErrorsMarkdownTableRows,
} from '../../../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers';
import * as i18n from './translations';
import type { ErrorSummary } from '../../../../types';
import { ERROR, INDEX, PATTERN } from './errors_viewer/translations';
import { getErrorsMarkdownTable, getErrorsMarkdownTableRows } from '../../utils/markdown';
import { ERROR, ERRORS, PATTERN } from '../../translations';
import { COPIED_ERRORS_TOAST_TITLE, VIEW_ERRORS } from './translations';
const CallOut = styled(EuiCallOut)`
max-width: ${ERRORS_CONTAINER_MAX_WIDTH}px;
@ -45,28 +54,28 @@ const ErrorsPopoverComponent: React.FC<Props> = ({ addSuccessToast, errorSummary
errorSummary,
getMarkdownTableRows: getErrorsMarkdownTableRows,
headerNames: [PATTERN, INDEX, ERROR],
title: i18n.ERRORS,
title: ERRORS,
});
copyToClipboard(markdown);
closePopover();
addSuccessToast({
title: i18n.COPIED_ERRORS_TOAST_TITLE,
title: COPIED_ERRORS_TOAST_TITLE,
});
}, [addSuccessToast, closePopover, errorSummary]);
const button = useMemo(
() => (
<EuiButtonEmpty
aria-label={i18n.VIEW_ERRORS}
aria-label={VIEW_ERRORS}
data-test-subj="viewErrors"
disabled={errorSummary.length === 0}
flush="both"
onClick={onClick}
size="xs"
>
{i18n.VIEW_ERRORS}
{VIEW_ERRORS}
</EuiButtonEmpty>
),
[errorSummary.length, onClick]
@ -80,32 +89,32 @@ const ErrorsPopoverComponent: React.FC<Props> = ({ addSuccessToast, errorSummary
isOpen={isPopoverOpen}
panelPaddingSize="s"
>
<CallOut color="danger" data-test-subj="callout" size="s" title={i18n.ERRORS}>
<p>{i18n.ERRORS_CALLOUT_SUMMARY}</p>
<CallOut color="danger" data-test-subj="callout" size="s" title={ERRORS}>
<p>{ERRORS_CALLOUT_SUMMARY}</p>
<p>{i18n.ERRORS_MAY_OCCUR}</p>
<p>{ERRORS_MAY_OCCUR}</p>
<span>{i18n.THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED}</span>
<span>{THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED}</span>
<ul>
<li>
<EuiCode>{i18n.MONITOR}</EuiCode> {i18n.OR} <EuiCode>{i18n.MANAGE}</EuiCode>
<EuiCode>{MONITOR}</EuiCode> {OR} <EuiCode>{MANAGE}</EuiCode>
</li>
<li>
<EuiCode>{i18n.VIEW_INDEX_METADATA}</EuiCode>
<EuiCode>{VIEW_INDEX_METADATA}</EuiCode>
</li>
<li>
<EuiCode>{i18n.READ}</EuiCode>
<EuiCode>{READ}</EuiCode>
</li>
</ul>
<EuiButtonEmpty
aria-label={i18n.COPY_TO_CLIPBOARD}
aria-label={COPY_TO_CLIPBOARD}
data-test-subj="copyToClipboard"
flush="both"
onClick={onCopy}
size="xs"
>
{i18n.COPY_TO_CLIPBOARD}
{COPY_TO_CLIPBOARD}
</EuiButtonEmpty>
</CallOut>

View file

@ -14,74 +14,9 @@ export const COPIED_ERRORS_TOAST_TITLE = i18n.translate(
}
);
export const COPY_TO_CLIPBOARD = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.copyToClipboardButton',
{
defaultMessage: 'Copy to clipboard',
}
);
export const ERRORS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsTitle',
{
defaultMessage: 'Errors',
}
);
export const ERRORS_CALLOUT_SUMMARY = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsCalloutSummary',
{
defaultMessage: 'Some indices were not checked for Data Quality',
}
);
export const ERRORS_MAY_OCCUR = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.errorMayOccurLabel',
{
defaultMessage:
"Errors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for access",
}
);
export const MANAGE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.manage',
{
defaultMessage: 'manage',
}
);
export const MONITOR = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.monitor',
{
defaultMessage: 'monitor',
}
);
export const OR = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.errors.or', {
defaultMessage: 'or',
});
export const READ = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.errors.read', {
defaultMessage: 'read',
});
export const THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel',
{
defaultMessage: 'The following privileges are required to check an index:',
}
);
export const VIEW_ERRORS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.viewErrorsButton',
{
defaultMessage: 'View errors',
}
);
export const VIEW_INDEX_METADATA = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata',
{
defaultMessage: 'view_index_metadata',
}
);

View file

@ -12,16 +12,6 @@ import styled from 'styled-components';
import { CheckAll } from './check_all';
import { CheckStatus } from './check_status';
import { ERROR, INDEX, PATTERN } from './check_status/errors_popover/errors_viewer/translations';
import { ERRORS } from './check_status/errors_popover/translations';
import {
getDataQualitySummaryMarkdownComment,
getErrorsMarkdownTable,
getErrorsMarkdownTableRows,
getPatternSummaryMarkdownComment,
getSummaryTableMarkdownHeader,
getSummaryTableMarkdownRow,
} from '../../data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers';
import type { DataQualityCheckResult, IndexToCheck, PatternRollup } from '../../types';
import { useDataQualityContext } from '../../data_quality_context';
import { useResultsRollupContext } from '../../contexts/results_rollup_context';
@ -30,6 +20,15 @@ import { getErrorSummaries } from './utils/get_error_summaries';
import { getSizeInBytes } from '../../utils/stats';
import { getSummaryTableItems } from '../../utils/get_summary_table_items';
import { defaultSort } from '../../constants';
import {
getDataQualitySummaryMarkdownComment,
getErrorsMarkdownTable,
getErrorsMarkdownTableRows,
getPatternSummaryMarkdownComment,
} from './utils/markdown';
import { getSummaryTableMarkdownHeader, getSummaryTableMarkdownRow } from '../../utils/markdown';
import { ERROR, ERRORS, PATTERN } from './translations';
import { INDEX } from '../../translations';
const StyledActionsContainerFlexItem = styled(EuiFlexItem)`
margin-top: auto;

View file

@ -7,22 +7,29 @@
import { i18n } from '@kbn/i18n';
export const DATA_QUALITY = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.dataQuality',
{
defaultMessage: 'Data quality',
}
);
export const ERROR = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.errorColumn',
'securitySolutionPackages.ecsDataQualityDashboard.errors.error',
{
defaultMessage: 'Error',
}
);
export const INDEX = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.indexColumn',
export const ERRORS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.errors',
{
defaultMessage: 'Index',
defaultMessage: 'Errors',
}
);
export const PATTERN = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.patternColumn',
'securitySolutionPackages.ecsDataQualityDashboard.errors.pattern',
{
defaultMessage: 'Pattern',
}

View file

@ -0,0 +1,212 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor 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 { EMPTY_STAT } from '../../../constants';
import {
auditbeatNoResults,
auditbeatWithAllResults,
} from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
import { INDEX } from '../../../translations';
import { ErrorSummary, PatternRollup } from '../../../types';
import { ERROR, ERRORS, PATTERN } from '../translations';
import {
getDataQualitySummaryMarkdownComment,
getErrorsMarkdownTable,
getErrorsMarkdownTableRows,
getIlmExplainPhaseCountsMarkdownComment,
getPatternSummaryMarkdownComment,
} from './markdown';
const defaultBytesFormat = '0,0.[0]b';
const formatBytes = (value: number | undefined) =>
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
const defaultNumberFormat = '0,0.[000]';
const formatNumber = (value: number | undefined) =>
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
describe('getIlmExplainPhaseCountsMarkdownComment', () => {
test('it returns the expected comment when _all_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 99,
warm: 8,
unmanaged: 77,
cold: 6,
frozen: 55,
})
).toEqual('`hot(99)` `warm(8)` `unmanaged(77)` `cold(6)` `frozen(55)`');
});
test('it returns the expected comment when _some_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 9,
warm: 0,
unmanaged: 2,
cold: 1,
frozen: 0,
})
).toEqual('`hot(9)` `unmanaged(2)` `cold(1)`');
});
test('it returns the expected comment when _none_ of the counts are greater than zero', () => {
expect(
getIlmExplainPhaseCountsMarkdownComment({
hot: 0,
warm: 0,
unmanaged: 0,
cold: 0,
frozen: 0,
})
).toEqual('');
});
});
describe('getPatternSummaryMarkdownComment', () => {
test('it returns the expected comment when the rollup contains results for all of the indices in the pattern', () => {
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: auditbeatWithAllResults,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when the rollup contains no results', () => {
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: auditbeatNoResults,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| -- | 0 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when the rollup does NOT have `ilmExplainPhaseCounts`', () => {
const noIlmExplainPhaseCounts: PatternRollup = {
...auditbeatWithAllResults,
ilmExplainPhaseCounts: undefined, // <--
};
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: noIlmExplainPhaseCounts,
})
).toEqual(
'## auditbeat-*\n\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n'
);
});
test('it returns the expected comment when `docsCount` is undefined', () => {
const noDocsCount: PatternRollup = {
...auditbeatWithAllResults,
docsCount: undefined, // <--
};
expect(
getPatternSummaryMarkdownComment({
formatBytes,
formatNumber,
patternRollup: noDocsCount,
})
).toEqual(
'## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 0 |\n\n'
);
});
});
describe('getDataQualitySummaryMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getDataQualitySummaryMarkdownComment({
formatBytes,
formatNumber,
totalDocsCount: 3343719,
totalIncompatible: 4,
totalIndices: 30,
totalIndicesChecked: 2,
sizeInBytes: 4294967296,
})
).toEqual(
'# Data quality\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 2 | 30 | 4GB | 3,343,719 |\n\n'
);
});
test('it returns the expected comment when optional values are undefined', () => {
expect(
getDataQualitySummaryMarkdownComment({
formatBytes,
formatNumber,
totalDocsCount: undefined,
totalIncompatible: undefined,
totalIndices: undefined,
totalIndicesChecked: undefined,
sizeInBytes: undefined,
})
).toEqual(
'# Data quality\n\n| Incompatible fields | Indices checked | Indices | Docs |\n|---------------------|-----------------|---------|------|\n| -- | -- | -- | 0 |\n\n'
);
});
});
const errorSummary: ErrorSummary[] = [
{
pattern: '.alerts-security.alerts-default',
indexName: null,
error: 'Error loading stats: Error: Forbidden',
},
{
error:
'Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden',
indexName: 'auditbeat-7.2.1-2023.02.13-000001',
pattern: 'auditbeat-*',
},
];
describe('getErrorsMarkdownTableRows', () => {
test('it returns the expected markdown table rows', () => {
expect(getErrorsMarkdownTableRows(errorSummary)).toEqual(
'| .alerts-security.alerts-default | -- | `Error loading stats: Error: Forbidden` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | `Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden` |'
);
});
});
describe('getErrorsMarkdownTable', () => {
test('it returns the expected table contents', () => {
expect(
getErrorsMarkdownTable({
errorSummary,
getMarkdownTableRows: getErrorsMarkdownTableRows,
headerNames: [PATTERN, INDEX, ERROR],
title: ERRORS,
})
).toEqual(
`## Errors\n\nSome indices were not checked for Data Quality\n\nErrors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for access\n\nThe following privileges are required to check an index:\n- \`monitor\` or \`manage\`\n- \`view_index_metadata\`\n- \`read\`\n\n\n| Pattern | Index | Error | \n|---------|-------|-------|\n| .alerts-security.alerts-default | -- | \`Error loading stats: Error: Forbidden\` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | \`Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden\` |\n`
);
});
test('it returns an empty string when the error summary is empty', () => {
expect(
getErrorsMarkdownTable({
errorSummary: [], // <-- empty
getMarkdownTableRows: getErrorsMarkdownTableRows,
headerNames: [PATTERN, INDEX, ERROR],
title: ERRORS,
})
).toEqual('');
});
});

View file

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor 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 { EMPTY_PLACEHOLDER } from '../../../constants';
import {
COLD,
ERRORS_CALLOUT_SUMMARY,
ERRORS_MAY_OCCUR,
FROZEN,
HOT,
MANAGE,
MONITOR,
OR,
READ,
THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED,
UNMANAGED,
VIEW_INDEX_METADATA,
WARM,
} from '../../../translations';
import { ErrorSummary, IlmExplainPhaseCounts, PatternRollup } from '../../../types';
import {
escapeNewlines,
getCodeFormattedValue,
getMarkdownTableHeader,
getStatsRollupMarkdownComment,
} from '../../../utils/markdown';
import { getTotalPatternIncompatible, getTotalPatternIndicesChecked } from '../../../utils/stats';
import { DATA_QUALITY } from '../translations';
export const getIlmExplainPhaseCountsMarkdownComment = ({
hot,
warm,
unmanaged,
cold,
frozen,
}: IlmExplainPhaseCounts): string =>
[
hot > 0 ? getCodeFormattedValue(`${HOT}(${hot})`) : '',
warm > 0 ? getCodeFormattedValue(`${WARM}(${warm})`) : '',
unmanaged > 0 ? getCodeFormattedValue(`${UNMANAGED}(${unmanaged})`) : '',
cold > 0 ? getCodeFormattedValue(`${COLD}(${cold})`) : '',
frozen > 0 ? getCodeFormattedValue(`${FROZEN}(${frozen})`) : '',
]
.filter((x) => x !== '') // prevents extra whitespace
.join(' ');
export const getPatternSummaryMarkdownComment = ({
formatBytes,
formatNumber,
patternRollup,
patternRollup: { docsCount, indices, ilmExplainPhaseCounts, pattern, results },
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
patternRollup: PatternRollup;
}): string =>
`## ${escapeNewlines(pattern)}
${
ilmExplainPhaseCounts != null
? getIlmExplainPhaseCountsMarkdownComment(ilmExplainPhaseCounts)
: ''
}
${getStatsRollupMarkdownComment({
docsCount: docsCount ?? 0,
formatBytes,
formatNumber,
incompatible: getTotalPatternIncompatible(results),
indices,
indicesChecked: getTotalPatternIndicesChecked(patternRollup),
sizeInBytes: patternRollup.sizeInBytes,
})}
`;
export const getDataQualitySummaryMarkdownComment = ({
formatBytes,
formatNumber,
totalDocsCount,
totalIncompatible,
totalIndices,
totalIndicesChecked,
sizeInBytes,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
totalDocsCount: number | undefined;
totalIncompatible: number | undefined;
totalIndices: number | undefined;
totalIndicesChecked: number | undefined;
sizeInBytes: number | undefined;
}): string =>
`# ${DATA_QUALITY}
${getStatsRollupMarkdownComment({
docsCount: totalDocsCount ?? 0,
formatBytes,
formatNumber,
incompatible: totalIncompatible,
indices: totalIndices,
indicesChecked: totalIndicesChecked,
sizeInBytes,
})}
`;
export const getErrorsMarkdownTable = ({
errorSummary,
getMarkdownTableRows,
headerNames,
title,
}: {
errorSummary: ErrorSummary[];
getMarkdownTableRows: (errorSummary: ErrorSummary[]) => string;
headerNames: string[];
title: string;
}): string =>
errorSummary.length > 0
? `## ${escapeNewlines(title)}
${ERRORS_CALLOUT_SUMMARY}
${ERRORS_MAY_OCCUR}
${THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED}
- \`${MONITOR}\` ${OR} \`${MANAGE}\`
- \`${VIEW_INDEX_METADATA}\`
- \`${READ}\`
${getMarkdownTableHeader(headerNames)}
${getMarkdownTableRows(errorSummary)}
`
: '';
export const getErrorsMarkdownTableRows = (errorSummary: ErrorSummary[]): string =>
errorSummary
.map(
({ pattern, indexName, error }) =>
`| ${escapeNewlines(pattern)} | ${escapeNewlines(
indexName ?? EMPTY_PLACEHOLDER
)} | ${getCodeFormattedValue(error)} |`
)
.join('\n');

View file

@ -85,13 +85,6 @@ export const DATA_QUALITY_SUGGESTED_USER_PROMPT = i18n.translate(
}
);
export const DATA_QUALITY_TITLE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardTitle',
{
defaultMessage: 'Data quality',
}
);
export const DEFAULT_PANEL_TITLE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle',
{
@ -190,6 +183,13 @@ export const ILM_PHASE: string = i18n.translate(
}
);
export const ILM_PHASE_CAPITALIZED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCapitalized',
{
defaultMessage: 'ILM Phase',
}
);
export const LAST_CHECKED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.lastCheckedLabel',
{
@ -305,3 +305,87 @@ export const AN_ERROR_OCCURRED_CHECKING_INDEX = (indexName: string) =>
defaultMessage: 'An error occurred checking index {indexName}',
}
);
export const DOCS = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.docs', {
defaultMessage: 'Docs',
});
export const INCOMPATIBLE_FIELDS = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.incompatibleFields',
{
defaultMessage: 'Incompatible fields',
}
);
export const INDICES = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.indices', {
defaultMessage: 'Indices',
});
export const INDICES_CHECKED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.indicesChecked',
{
defaultMessage: 'Indices checked',
}
);
export const INDEX = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.index', {
defaultMessage: 'Index',
});
export const SIZE = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.size', {
defaultMessage: 'Size',
});
export const RESULT = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.result', {
defaultMessage: 'Result',
});
export const ERRORS_CALLOUT_SUMMARY = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.errorsCalloutSummary',
{
defaultMessage: 'Some indices were not checked for Data Quality',
}
);
export const ERRORS_MAY_OCCUR = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.errorMayOccurLabel',
{
defaultMessage:
"Errors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for access",
}
);
export const MANAGE = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.manage',
{
defaultMessage: 'manage',
}
);
export const MONITOR = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.monitor',
{
defaultMessage: 'monitor',
}
);
export const OR = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.errors.or', {
defaultMessage: 'or',
});
export const READ = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.errors.read', {
defaultMessage: 'read',
});
export const THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel',
{
defaultMessage: 'The following privileges are required to check an index:',
}
);
export const VIEW_INDEX_METADATA = i18n.translate(
'securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata',
{
defaultMessage: 'view_index_metadata',
}
);

View file

@ -0,0 +1,208 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import numeral from '@elastic/numeral';
import { EMPTY_STAT } from '../constants';
import {
getCodeFormattedValue,
getHeaderSeparator,
getMarkdownTableHeader,
getResultEmoji,
getStatsRollupMarkdownComment,
getSummaryTableMarkdownHeader,
getSummaryTableMarkdownRow,
} from './markdown';
const defaultBytesFormat = '0,0.[0]b';
const formatBytes = (value: number | undefined) =>
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
const defaultNumberFormat = '0,0.[000]';
const formatNumber = (value: number | undefined) =>
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
describe('getHeaderSeparator', () => {
test('it returns a sequence of dashes equal to the length of the header, plus two additional dashes to pad each end of the cntent', () => {
const content = '0123456789'; // content.length === 10
const expected = '------------'; // expected.length === 12
expect(getHeaderSeparator(content)).toEqual(expected);
});
});
describe('getCodeFormattedValue', () => {
test('it returns the expected placeholder when `value` is undefined', () => {
expect(getCodeFormattedValue(undefined)).toEqual('`--`');
});
test('it returns the content formatted as markdown code', () => {
const value = 'foozle';
expect(getCodeFormattedValue(value)).toEqual('`foozle`');
});
test('it escapes content such that `value` may be included in a markdown table cell', () => {
const value =
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n';
expect(getCodeFormattedValue(value)).toEqual(
'`\\| there were newlines and column separators in the beginning, middle, \\|and end\\| `'
);
});
});
describe('getStatsRollupMarkdownComment', () => {
test('it returns the expected comment', () => {
expect(
getStatsRollupMarkdownComment({
docsCount: 57410,
formatBytes,
formatNumber,
incompatible: 3,
indices: 25,
indicesChecked: 1,
sizeInBytes: 28413,
})
).toEqual(
'| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 3 | 1 | 25 | 27.7KB | 57,410 |\n'
);
});
test('it returns the expected comment when optional values are undefined', () => {
expect(
getStatsRollupMarkdownComment({
docsCount: 0,
formatBytes,
formatNumber,
incompatible: undefined,
indices: undefined,
indicesChecked: undefined,
sizeInBytes: undefined,
})
).toEqual(
'| Incompatible fields | Indices checked | Indices | Docs |\n|---------------------|-----------------|---------|------|\n| -- | -- | -- | 0 |\n'
);
});
});
describe('getSummaryTableMarkdownHeader', () => {
test('it returns the expected header', () => {
const isILMAvailable = true;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|'
);
});
test('it returns the expected header when isILMAvailable is false', () => {
const isILMAvailable = false;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|'
);
});
test('it returns the expected header when displayDocSize is false', () => {
const isILMAvailable = false;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|'
);
});
});
describe('getSummaryTableMarkdownRow', () => {
test('it returns the expected row when all values are provided', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: 3,
ilmPhase: 'unmanaged',
isILMAvailable: true,
indexName: 'auditbeat-custom-index-1',
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n');
});
test('it returns the expected row when optional values are NOT provided', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- | -- | 27.7KB |\n');
});
test('it returns the expected row when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- |\n');
});
test('it returns the expected row when sizeInBytes is undefined', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
patternDocsCount: 57410,
sizeInBytes: undefined,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- |\n');
});
});
describe('getResultEmoji', () => {
test('it returns the expected placeholder when `incompatible` is undefined', () => {
expect(getResultEmoji(undefined)).toEqual('--');
});
test('it returns a ✅ when the incompatible count is zero', () => {
expect(getResultEmoji(0)).toEqual('✅');
});
test('it returns a ❌ when the incompatible count is NOT zero', () => {
expect(getResultEmoji(1)).toEqual('❌');
});
});
describe('getMarkdownTableHeader', () => {
const headerNames = [
'|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n',
'A second column',
'A third column',
];
test('it returns the expected table header', () => {
expect(getMarkdownTableHeader(headerNames)).toEqual(
'\n| \\| there were newlines and column separators in the beginning, middle, \\|and end\\| | A second column | A third column | \n|----------------------------------------------------------------------------------|-----------------|----------------|'
);
});
});

View file

@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor 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 { EMPTY_PLACEHOLDER, EMPTY_STAT } from '../constants';
import {
DOCS,
ILM_PHASE,
ILM_PHASE_CAPITALIZED,
INCOMPATIBLE_FIELDS,
INDEX,
INDICES,
INDICES_CHECKED,
RESULT,
SIZE,
} from '../translations';
import { IlmPhase } from '../types';
import { getDocsCountPercent } from './stats';
export const escapeNewlines = (content: string | undefined): string | undefined =>
content != null ? content.replaceAll('\n', ' ').replaceAll('|', '\\|') : content;
export const getCodeFormattedValue = (value: string | undefined) =>
`\`${escapeNewlines(value ?? EMPTY_PLACEHOLDER)}\``;
export const getHeaderSeparator = (headerText: string): string => '-'.repeat(headerText.length + 2); // 2 extra, for the spaces on both sides of the column name
export const getStatsRollupMarkdownComment = ({
docsCount,
formatBytes,
formatNumber,
incompatible,
indices,
indicesChecked,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
incompatible: number | undefined;
indices: number | undefined;
indicesChecked: number | undefined;
sizeInBytes: number | undefined;
}): string =>
Number.isInteger(sizeInBytes)
? `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${SIZE} | ${DOCS} |
|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
INDICES_CHECKED
)}|${getHeaderSeparator(INDICES)}|${getHeaderSeparator(SIZE)}|${getHeaderSeparator(DOCS)}|
| ${incompatible ?? EMPTY_STAT} | ${indicesChecked ?? EMPTY_STAT} | ${
indices ?? EMPTY_STAT
} | ${formatBytes(sizeInBytes)} | ${formatNumber(docsCount)} |
`
: `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${DOCS} |
|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
INDICES_CHECKED
)}|${getHeaderSeparator(INDICES)}|${getHeaderSeparator(DOCS)}|
| ${incompatible ?? EMPTY_STAT} | ${indicesChecked ?? EMPTY_STAT} | ${
indices ?? EMPTY_STAT
} | ${formatNumber(docsCount)} |
`;
export const getSummaryTableMarkdownHeader = (includeDocSize: boolean): string =>
includeDocSize
? `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE_CAPITALIZED} | ${SIZE} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
ILM_PHASE
)}|${getHeaderSeparator(SIZE)}|`
: `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|`;
export const getResultEmoji = (incompatible: number | undefined): string => {
if (incompatible == null) {
return EMPTY_PLACEHOLDER;
} else {
return incompatible === 0 ? '✅' : '❌';
}
};
export const getSummaryTableMarkdownRow = ({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
incompatible,
indexName,
isILMAvailable,
patternDocsCount,
sizeInBytes,
}: {
docsCount: number;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
incompatible: number | undefined;
indexName: string;
isILMAvailable: boolean;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
isILMAvailable && Number.isInteger(sizeInBytes)
? `| ${getResultEmoji(incompatible)} | ${escapeNewlines(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${
ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER
} | ${formatBytes(sizeInBytes)} |
`
: `| ${getResultEmoji(incompatible)} | ${escapeNewlines(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} |
`;
export const getMarkdownTableHeader = (headerNames: string[]) => `
| ${headerNames.map((name) => `${escapeNewlines(name)} | `).join('')}
|${headerNames.map((name) => `${getHeaderSeparator(name)}|`).join('')}`;

View file

@ -15,6 +15,7 @@ import { mockStatsAuditbeatIndex } from '../mock/stats/mock_stats_packetbeat_ind
import { DataQualityCheckResult } from '../types';
import {
getDocsCount,
getDocsCountPercent,
getSizeInBytes,
getTotalPatternIncompatible,
getTotalPatternIndicesChecked,
@ -250,3 +251,34 @@ describe('getTotalPatternIncompatible', () => {
expect(getTotalPatternIncompatible(undefined)).toBeUndefined();
});
});
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

@ -59,3 +59,20 @@ export const getTotalPatternIncompatible = (
return allResults.reduce<number>((acc, { incompatible }) => acc + (incompatible ?? 0), 0);
};
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

@ -20,4 +20,8 @@ export {
DATA_QUALITY_DASHBOARD_CONVERSATION_ID,
} from './impl/data_quality_panel/translations';
export { ECS_REFERENCE_URL } from './impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index_properties/markdown/helpers';
export {
ECS_REFERENCE_URL,
ECS_FIELD_REFERENCE_URL,
MAPPING_URL,
} from './impl/data_quality_panel/constants';

View file

@ -6851,13 +6851,7 @@
"securitySolutionPackages.ecsDataQualityDashboard.errors.read": "lire",
"securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel": "Les privilèges suivants sont requis pour vérifier un index :",
"securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata": "view_index_metadata",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.copyToClipboardButton": "Copier dans le presse-papiers",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsCalloutSummary": "La qualité des données n'a pas été vérifiée pour certains index",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsTitle": "Erreurs",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.viewErrorsButton": "Afficher les erreurs",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.errorColumn": "Erreur",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.indexColumn": "Index",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.patternColumn": "Modèle",
"securitySolutionPackages.ecsDataQualityDashboard.fieldsLabel": "Champs",
"securitySolutionPackages.ecsDataQualityDashboard.frozenDescription": "L'index n'est plus mis à jour et il est rarement interrogé. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient extrêmement lentes.",
"securitySolutionPackages.ecsDataQualityDashboard.frozenPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"frozen\". Les index gelés ne sont plus mis à jour et sont rarement interrogés. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient extrêmement lentes.",
@ -6869,15 +6863,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "hot",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "Phase ILM",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "La qualité des données sera vérifiée pour les index comprenant ces phases de gestion du cycle de vie des index (ILM, Index Lifecycle Management)",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel": "froid",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptFrozenLabel": "frozen",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptHotLabel": "hot",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCanBeCheckedSubtitle": "Phases ILM dans lesquelles la qualité des données peut être vérifiée",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCannotBeCheckedSubtitle": "Phases ILM dans lesquelles la vérification ne peut pas être effectuée",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptITheFollowingIlmPhasesLabel": "Les phases ILM suivantes ne sont pas disponibles pour la vérification de la qualité des données, car leur accès est plus lent",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "Sélectionner une ou plusieurs phases ILM",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "non géré",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "warm",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "non géré",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "warm",
"securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "Mappings de champ incompatibles  {indexName}",
@ -6899,7 +6888,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ Les règles de moteur de détection personnalisées fonctionnent",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ Les règles de moteur de détection fonctionneront pour ces champs",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ Les règles de moteur de détection référençant ces champs ne leur correspondront peut-être pas correctement",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.docsLabel": "Documents",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {Le type de mapping d'index et les valeurs de document de ce champ sont conformes} other {Les types de mapping d'index et les valeurs de document de ces champs sont conformes}} à la version {version} d'Elastic Common Schema (ECS)",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Champ conforme} other {Champs conformes}} à ECS",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "Aucun mapping de champ de cet index n'est conforme à Elastic Common Schema (ECS). L'index doit (au moins) contenir un champ de date @timestamp.",
@ -6914,7 +6902,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Champ incompatible} other {Champs incompatibles}}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "Tous les mappings de champs et toutes les valeurs de documents de cet index sont conformes à Elastic Common Schema (ECS).",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "Toutes les valeurs et tous les mappings de champs sont conformes à ECS",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleFieldsTab": "Champs incompatibles",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "Index",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ Les mappings ou valeurs de champs qui ne sont pas conformes à ECS ne sont pas pris en charge",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "Veuillez envisager d'ajouter un mapping de champ de @timestamp (date) à cet index, comme requis par Elastic Common Schema (ECS), car :",
@ -6945,7 +6932,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel": "même famille",
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle": "Mêmes familles de mappings de champ  {indexName}",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardSubtitle": "Vérifiez la compatibilité des mappings et des valeurs d'index avec",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardTitle": "Qualité des données",
"securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "Sélectionner un index pour le comparer à la version ECS",
"securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "Sélectionner une ou plusieurs phases ILM",
"securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "vérifié",
@ -6961,18 +6947,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "Aucune donnée à afficher",
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "Le champ {stackByField1} n'était présent dans aucun groupe",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "Réduire",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "Documents",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "Développer les lignes",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "Phase ILM",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "Champs incompatibles",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "Index",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexesNameLabel": "Nom de l'index",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexToolTip": "Cet index correspond au nom d'index ou de modèle : {pattern}",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "Index vérifiés",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "Index",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "Dernière vérification",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "Résultat",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "Taille",
"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

@ -6847,13 +6847,7 @@
"securitySolutionPackages.ecsDataQualityDashboard.errors.read": "読み取り",
"securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel": "インデックスを確認するには次の権限が必要です:",
"securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata": "view_index_metadata",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.copyToClipboardButton": "クリップボードにコピー",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsCalloutSummary": "一部のインデックスのデータ品質が確認されませんでした",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsTitle": "エラー",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.viewErrorsButton": "エラーを表示",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.errorColumn": "エラー",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.indexColumn": "インデックス",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.patternColumn": "パターン",
"securitySolutionPackages.ecsDataQualityDashboard.fieldsLabel": "フィールド",
"securitySolutionPackages.ecsDataQualityDashboard.frozenDescription": "インデックスは更新されず、ほとんど照会されません。情報はまだ検索可能でなければなりませんが、クエリーが非常に低速でも問題ありません。",
"securitySolutionPackages.ecsDataQualityDashboard.frozenPatternTooltip": "{pattern}パターンと一致する{indices} {indices, plural, other {インデックス}}{indices, plural, other {は}}フローズンです。フローズンインデックスは更新されず、ほとんど照会されません。情報はまだ検索可能でなければなりませんが、クエリーが非常に低速でも問題ありません。",
@ -6865,15 +6859,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "ホット",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "ILMフェーズ",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "これらのインデックスライフサイクル管理ILMフェーズのインデックスはデータ品質が確認されます",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel": "コールド",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptFrozenLabel": "凍結",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptHotLabel": "ホット",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCanBeCheckedSubtitle": "データ品質を確認できるILMフェーズ",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCannotBeCheckedSubtitle": "確認できないILMフェーズ",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptITheFollowingIlmPhasesLabel": "次のILMフェーズは、アクセスが低速になるため、データ品質が確認できません",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "1つ以上のILMフェーズを選択",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "管理対象外",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "ウォーム",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "管理対象外",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "ウォーム",
"securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "非互換フィールドマッピング - {indexName}",
@ -6895,7 +6884,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ カスタム検出エンジンルールが動作する",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ これらのフィールドの検出エンジンルールが動作する",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ これらのフィールドを参照する検出エンジンルールが正常に一致しない場合がある",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.docsLabel": "ドキュメント",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {このフィールドのインデックスマッピングタイプとドキュメント値} other {これらのフィールドのインデックスマッピングタイプとドキュメント値}}は、Elastic Common SchemaECSバージョン{version}に準拠しています",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠したフィールド}}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "このインデックスのどのフィールドマッピングもElastic Common SchemaECSと互換性がありません。インデックスには1つ以上の@timestamp日付フィールドを含める必要があります。",
@ -6910,7 +6898,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠していないフィールド}}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "このインデックスのすべてのフィールドマッピングとドキュメント値がElastic Common SchemaECSと互換性があります。",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "すべてのフィールドマッピングと値がECSと互換性があります",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleFieldsTab": "非互換フィールド",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "インデックス",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ ECSと互換性がないマッピングまたはフィールド値はサポートされません",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "次の理由のため、Elastic Common SchemaECSで必要な@timestamp日付フィールドマッピングをこのインデックスに追加することを検討してください。",
@ -6941,7 +6928,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel": "同じファミリー",
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle": "同じファミリーフィールドマッピング - {indexName}",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardSubtitle": "互換性に関してインデックスマッピングと値を確認",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardTitle": "データ品質",
"securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "ECSバージョンと比較するインデックスを選択",
"securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "1つ以上のILMフェーズを選択",
"securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "確認済み",
@ -6957,18 +6943,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "表示するデータがありません",
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "{stackByField1}フィールドがどのグループにも存在しませんでした",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "縮小",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "ドキュメント",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "行を展開",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILMフェーズ",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "非互換フィールド",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "インデックス",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexesNameLabel": "インデックス名",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexToolTip": "このインデックスはパターンまたはインデックス名と一致します:{pattern}",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "確認されたインデックス",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "インデックス",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "最終確認",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "結果",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "サイズ",
"securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "イベントが生成された日時これはイベントから抽出された日時で、一般的にはイベントがソースから生成された日時を表します。イベントソースに元のタイムスタンプがない場合は、通常、この値はイベントがパイプラインによって受信された最初の日時が入力されます。すべてのイベントの必須フィールドです。",
"securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "エラーをクリップボードにコピーしました",
"securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "結果をクリップボードにコピーしました",

View file

@ -6858,13 +6858,7 @@
"securitySolutionPackages.ecsDataQualityDashboard.errors.read": "读取",
"securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel": "检查索引需要以下权限:",
"securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata": "view_index_metadata",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.copyToClipboardButton": "复制到剪贴板",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsCalloutSummary": "未检查某些索引以了解数据质量",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.errorsTitle": "错误",
"securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.viewErrorsButton": "查看错误",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.errorColumn": "错误",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.indexColumn": "索引",
"securitySolutionPackages.ecsDataQualityDashboard.errorsViewerTable.patternColumn": "模式",
"securitySolutionPackages.ecsDataQualityDashboard.fieldsLabel": "字段",
"securitySolutionPackages.ecsDataQualityDashboard.frozenDescription": "不再更新并且极少查询该索引。仍然需要能够搜索信息,但如果那些查询速度极慢,也没有关系。",
"securitySolutionPackages.ecsDataQualityDashboard.frozenPatternTooltip": "{indices} 个匹配 {pattern} 模式的{indices, plural, other {索引}}{indices, plural, other {为}}已冻结索引。不再更新并且极少会查询已冻结索引。仍然需要能够搜索信息,但如果那些查询速度极慢,也没有关系。",
@ -6876,15 +6870,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "热",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseLabel": "ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptBody": "将检查具有这些索引生命周期管理 (ILM) 阶段的索引以了解数据质量",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptColdLabel": "冷",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptFrozenLabel": "冻结",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptHotLabel": "热",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCanBeCheckedSubtitle": "可进行检查以了解数据质量的 ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptIlmPhasesThatCannotBeCheckedSubtitle": "无法检查的 ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptITheFollowingIlmPhasesLabel": "由于访问速度较慢,无法检查以下 ILM 阶段以了解数据质量",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "选择一个或多个 ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptUnmanagedLabel": "未受管",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptWarmLabel": "温",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "未受管",
"securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "温",
"securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.incompatibleFieldMappingsTableTitle": "不兼容的字段映射 - {indexName}",
@ -6906,7 +6895,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.custonDetectionEngineRulesWorkMessage": "✅ 定制检测引擎规则有效",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWillWorkMessage": "✅ 检测引擎规则将适用于这些字段",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.detectionEngineRulesWontWorkMessage": "❌ 引用这些字段的检测引擎规则可能无法与其正确匹配",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.docsLabel": "文档",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCallout": "{fieldCount, plural, =1 {此字段的索引映射类型和文档值} other {这些字段的索引映射类型和文档值}}遵循 Elastic Common Schema (ECS) 版本 {version}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} 个符合 ECS 规范的{fieldCount, plural, other {字段}}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "此索引中没有任何字段映射遵循 Elastic Common Schema (ECS)。此索引必须(至少)包含一个 @timestamp 日期字段。",
@ -6921,7 +6909,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} 个不兼容的{fieldCount, plural, other {字段}}",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "此索引中的所有字段映射和文档值均符合 Elastic Common Schema (ECS) 规范。",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "所有字段映射和值均符合 ECS 规范",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleFieldsTab": "不兼容的字段",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "索引",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ 不支持不符合 ECS 规范的映射或字段值",
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "考虑根据 Elastic Common Schema (ECS) 的要求将 @timestamp日期字段映射添加到此索引因为",
@ -6952,7 +6939,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel": "同一系列",
"securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle": "同一系列的字段映射 - {indexName}",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardSubtitle": "检查索引映射和值以了解与以下项的兼容性",
"securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardTitle": "数据质量",
"securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "选择索引以将其与 ECS 版本进行比较",
"securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "选择一个或多个 ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "已检查",
@ -6968,18 +6954,10 @@
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "没有可显示的数据",
"securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "任何组中都不存在 {stackByField1} 字段",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "折叠",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.docsColumn": "文档",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "展开行",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.ilmPhaseColumn": "ILM 阶段",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.incompatibleFieldsColumn": "不兼容的字段",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexColumn": "索引",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexesNameLabel": "索引名称",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexToolTip": "此索引与模式或索引名称相匹配:{pattern}",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesCheckedColumn": "已检查索引",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indicesColumn": "索引",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "上次检查",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.resultColumn": "结果",
"securitySolutionPackages.ecsDataQualityDashboard.summaryTable.sizeColumn": "大小",
"securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "事件发生时的日期/时间。这是从事件中提取的日期/时间,通常表示源生成事件的时间。如果事件源没有原始时间戳,通常会在管道首次收到事件时填充此值。所有事件的必填字段。",
"securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "已将错误复制到剪贴板",
"securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "已将结果复制到剪贴板",