mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
096c52f096
commit
f79b714fda
40 changed files with 1454 additions and 1525 deletions
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
getMarkdownTable,
|
||||
getTabCountsMarkdownComment,
|
||||
getSummaryTableMarkdownComment,
|
||||
} from '../../../markdown/helpers';
|
||||
} from '../../utils/markdown';
|
||||
import * as i18n from '../../../translations';
|
||||
import type {
|
||||
CustomFieldMetadata,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SAME_FAMILY = i18n.translate(
|
||||
export const SAME_FAMILY_BADGE_LABEL = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'same family',
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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}`)}
|
||||
`;
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
})}
|
||||
`;
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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%');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
: '';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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('');
|
||||
});
|
||||
});
|
|
@ -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');
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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|----------------------------------------------------------------------------------|-----------------|----------------|'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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('')}`;
|
|
@ -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%');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
: '';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 Schema(ECS)バージョン{version}に準拠しています",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠したフィールド}}",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.ecsCompliantEmptyContent": "このインデックスのどのフィールドマッピングもElastic Common Schema(ECS)と互換性がありません。インデックスには(1つ以上の)@timestamp日付フィールドを含める必要があります。",
|
||||
|
@ -6910,7 +6898,6 @@
|
|||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, other {個のECSに準拠していないフィールド}}",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyContent": "このインデックスのすべてのフィールドマッピングとドキュメント値がElastic Common Schema(ECS)と互換性があります。",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleEmptyTitle": "すべてのフィールドマッピングと値がECSと互換性があります",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.incompatibleFieldsTab": "非互換フィールド",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.indexMarkdown": "インデックス",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.mappingThatConflictWithEcsMessage": "❌ ECSと互換性がないマッピングまたはフィールド値はサポートされません",
|
||||
"securitySolutionPackages.ecsDataQualityDashboard.indexProperties.missingTimestampCallout": "次の理由のため、Elastic Common Schema(ECS)で必要な@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": "結果をクリップボードにコピーしました",
|
||||
|
|
|
@ -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": "已将结果复制到剪贴板",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue