mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Secuity Solution][DQD] add list view and latest flyout (Phase 1) (#188468)
addresses #185881 ## Data Quality Dashboard UI Overhaul (Phase 1) This PR introduces UI changes to the Data Quality Dashboard, focusing on improving user experience and improving on existing data quality check process. ## Notable changes: - Update List View UI - Move in-row check expansion into a Flyout - Remove summary tab from index check - Update index check UI - Add table action to trigger manual individual index check without opening index check properties - Add in-flyout button to trigger manual individual check - Add additional index stats panel within flyout ## Notable technical changes: - remove prop drilling of new and existing omnipresent props by unifying them in a series of context providers at the top of DQ dashboard - introduce TestDataQualityProviders separate from external TestProviders (renamed to TestExternalProviders) in tests. Change affected tests. - introduce `useIndicesCheck` hook to unify logic of index checking to be able to call index checking from anywhere within DQD code without relying on flaky and complicated useEffect driven logic of state updates. - introduce `useIsMounted`, hook to resolve issues with react state update leaks - introduce throttled `useCurrentWindowWidth` to handle custom index flyout sizing for different screens. - cleanup and refactor whatever is directly related or affected by aforementioned code/test changes (including traces of removal of summary tab) - add extensive behavioral unit tests # UI Changes (Before / After) ## List View Layout made more spacious  ## Check All Layout made more spacious  ## Total Stats Rollup converted to badges  ## ILM Phase Filter repositioned  ## Pattern Title Section - Rearranged into accordion trigger button. Initially open by default. - IlmPhase badges and Pattern title are now stacked horizontally. - Index check result emoji converted to 'Pass' | 'Fail' badge - Pattern stats text rearranged as badged text. Incompatible fields show as red badges (when present), the rest - hollow.  ## Latest Pattern Indices Check Table - Added a new actions column with 2 actions (from left to right): - View details (replaces row expander functionality (and icon) and instead opens the index check results in a flyout) - Check now **(NEW)** (adds ability to inline check the index without opening it.) - Index check result emoji turned to 'Pass' | 'Fail' badge - `IlmPhase`, `Size`, `Last Check` columns width is shrunk to give more space for index name  ## Flyout Header - Added index name with result check badge as title - Added last check time as subtitle - Added Tabs section for Latest Check and History **(REMOVED in latest revision)**  ## Flyout Stats Panel - Added new index stats panel  ## Index Check Fields Tab - Tabs converted to a button group - Summary Tab is **REMOVED** - All field count badges have hollow color, except for red color for `incompatible fields` tab (when count > 0) and `ecs compliant fields` tab (when `@timestamp` is missing)  ## Index Check Fields Callouts - Callout header is removed (to avoid duplication with active tab name) - Actions are converted into sticky footer (shows when scrolled sticky to bottom, otherwise renders after the table) - Same for every index check fields tab  ## Compare Table List Values - List values in compare tables are now horizontally stacked instead of vertical to save space (applies to all compare tables in each index check fields tab)  ## Compare Table Columns - `ECS description` field width increased at the expense of `field` field, to make room for more readable description (applies respectively to all compare tables within index check fields tabs)  ## Flyout footer - Add `Check now` button, that checks currently open index again and updates the results in place. 
This commit is contained in:
parent
8d4704f1fe
commit
51764fa076
172 changed files with 6057 additions and 4987 deletions
|
@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { mockAllowedValues } from '../../mock/allowed_values/mock_allowed_values';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { EcsAllowedValues } from '.';
|
||||
|
||||
describe('EcsAllowedValues', () => {
|
||||
describe('when `allowedValues` exists', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<EcsAllowedValues allowedValues={mockAllowedValues} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -36,9 +36,9 @@ describe('EcsAllowedValues', () => {
|
|||
describe('when `allowedValues` is undefined', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<EcsAllowedValues allowedValues={undefined} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,16 +7,11 @@
|
|||
|
||||
import { EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EMPTY_PLACEHOLDER } from '../helpers';
|
||||
import { CodeSuccess } from '../../styles';
|
||||
import type { AllowedValue } from '../../types';
|
||||
|
||||
const EcsAllowedValueFlexItem = styled(EuiFlexItem)`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
allowedValues: AllowedValue[] | undefined;
|
||||
}
|
||||
|
@ -25,11 +20,11 @@ const EcsAllowedValuesComponent: React.FC<Props> = ({ allowedValues }) =>
|
|||
allowedValues == null ? (
|
||||
<EuiCode data-test-subj="ecsAllowedValuesEmpty">{EMPTY_PLACEHOLDER}</EuiCode>
|
||||
) : (
|
||||
<EuiFlexGroup data-test-subj="ecsAllowedValues" direction="column" gutterSize="none">
|
||||
<EuiFlexGroup data-test-subj="ecsAllowedValues" direction="row" wrap={true} gutterSize="xs">
|
||||
{allowedValues.map((x, i) => (
|
||||
<EcsAllowedValueFlexItem grow={false} key={`${x.name}_${i}`}>
|
||||
<EuiFlexItem grow={false} key={`${x.name}_${i}`}>
|
||||
<CodeSuccess>{x.name}</CodeSuccess>
|
||||
</EcsAllowedValueFlexItem>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
someField,
|
||||
eventCategoryWithUnallowedValues,
|
||||
} from '../../mock/enriched_field_metadata/mock_enriched_field_metadata';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
DOCUMENT_VALUES_ACTUAL,
|
||||
ECS_DESCRIPTION,
|
||||
|
@ -30,7 +30,7 @@ import { EMPTY_PLACEHOLDER, getCommonTableColumns } from '.';
|
|||
describe('getCommonTableColumns', () => {
|
||||
test('it returns the expected column configuration', () => {
|
||||
expect(getCommonTableColumns().map((x) => omit('render', x))).toEqual([
|
||||
{ field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '20%' },
|
||||
{ field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '15%' },
|
||||
{
|
||||
field: 'type',
|
||||
name: ECS_MAPPING_TYPE_EXPECTED,
|
||||
|
@ -64,7 +64,7 @@ describe('getCommonTableColumns', () => {
|
|||
name: ECS_DESCRIPTION,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '20%',
|
||||
width: '25%',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -76,9 +76,9 @@ describe('getCommonTableColumns', () => {
|
|||
const expected = 'keyword';
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{typeColumnRender != null && typeColumnRender(eventCategory.type, eventCategory)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected);
|
||||
|
@ -89,9 +89,9 @@ describe('getCommonTableColumns', () => {
|
|||
const typeColumnRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{typeColumnRender != null && typeColumnRender(undefined, eventCategory)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeSuccess')).toHaveTextContent(EMPTY_PLACEHOLDER);
|
||||
|
@ -113,13 +113,13 @@ describe('getCommonTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexFieldTypeColumnRender != null &&
|
||||
indexFieldTypeColumnRender(
|
||||
withTypeMismatchSameFamily.indexFieldType,
|
||||
withTypeMismatchSameFamily
|
||||
)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -146,13 +146,13 @@ describe('getCommonTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexFieldTypeColumnRender != null &&
|
||||
indexFieldTypeColumnRender(
|
||||
withTypeMismatchDifferentFamily.indexFieldType,
|
||||
withTypeMismatchDifferentFamily
|
||||
)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeDanger')).toHaveTextContent(indexFieldType);
|
||||
|
@ -165,10 +165,10 @@ describe('getCommonTableColumns', () => {
|
|||
const indexFieldTypeColumnRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexFieldTypeColumnRender != null &&
|
||||
indexFieldTypeColumnRender(eventCategory.indexFieldType, eventCategory)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeSuccess')).toHaveTextContent(eventCategory.indexFieldType);
|
||||
|
@ -187,10 +187,10 @@ describe('getCommonTableColumns', () => {
|
|||
: 'unexpected';
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{allowedValuesolumnRender != null &&
|
||||
allowedValuesolumnRender(eventCategory.allowed_values, eventCategory)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent(expectedAllowedValuesNames);
|
||||
|
@ -206,10 +206,10 @@ describe('getCommonTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{allowedValuesolumnRender != null &&
|
||||
allowedValuesolumnRender(undefined, withUndefinedAllowedValues)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ecsAllowedValuesEmpty')).toHaveTextContent(EMPTY_PLACEHOLDER);
|
||||
|
@ -222,13 +222,13 @@ describe('getCommonTableColumns', () => {
|
|||
const indexInvalidValuesRender = columns[4].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexInvalidValuesRender != null &&
|
||||
indexInvalidValuesRender(
|
||||
eventCategoryWithUnallowedValues.indexInvalidValues,
|
||||
eventCategoryWithUnallowedValues
|
||||
)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent(
|
||||
|
@ -249,10 +249,10 @@ describe('getCommonTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{descriptionolumnRender != null &&
|
||||
descriptionolumnRender(withDescription.description, withDescription)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('description')).toHaveTextContent(expectedDescription);
|
||||
|
@ -263,9 +263,9 @@ describe('getCommonTableColumns', () => {
|
|||
const descriptionolumnRender = columns[5].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{descriptionolumnRender != null && descriptionolumnRender(undefined, someField)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('emptyDescription')).toHaveTextContent(EMPTY_PLACEHOLDER);
|
||||
|
|
|
@ -27,7 +27,7 @@ export const getCommonTableColumns = (): Array<
|
|||
name: i18n.FIELD,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '20%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
|
@ -98,6 +98,6 @@ export const getCommonTableColumns = (): Array<
|
|||
),
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '20%',
|
||||
width: '25%',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -10,7 +10,7 @@ import { omit } from 'lodash/fp';
|
|||
import React from 'react';
|
||||
|
||||
import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { eventCategory } from '../../mock/enriched_field_metadata/mock_enriched_field_metadata';
|
||||
import { EcsBasedFieldMetadata } from '../../types';
|
||||
import { getIncompatibleMappingsTableColumns } from '.';
|
||||
|
@ -25,7 +25,7 @@ describe('getIncompatibleMappingsTableColumns', () => {
|
|||
name: 'Field',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
|
@ -46,7 +46,7 @@ describe('getIncompatibleMappingsTableColumns', () => {
|
|||
name: 'ECS description',
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -58,9 +58,9 @@ describe('getIncompatibleMappingsTableColumns', () => {
|
|||
const expected = 'keyword';
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{typeColumnRender != null && typeColumnRender(eventCategory.type, eventCategory)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeSuccess')).toHaveTextContent(expected);
|
||||
|
@ -82,13 +82,13 @@ describe('getIncompatibleMappingsTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexFieldTypeColumnRender != null &&
|
||||
indexFieldTypeColumnRender(
|
||||
withTypeMismatchSameFamily.indexFieldType,
|
||||
withTypeMismatchSameFamily
|
||||
)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -115,13 +115,13 @@ describe('getIncompatibleMappingsTableColumns', () => {
|
|||
};
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexFieldTypeColumnRender != null &&
|
||||
indexFieldTypeColumnRender(
|
||||
withTypeMismatchDifferentFamily.indexFieldType,
|
||||
withTypeMismatchDifferentFamily
|
||||
)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('codeDanger')).toHaveTextContent(indexFieldType);
|
||||
|
|
|
@ -23,7 +23,7 @@ export const getIncompatibleMappingsTableColumns = (): Array<
|
|||
name: i18n.FIELD,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
|
@ -54,6 +54,6 @@ export const getIncompatibleMappingsTableColumns = (): Array<
|
|||
name: i18n.ECS_DESCRIPTION,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
eventCategoryWithUnallowedValues,
|
||||
someField,
|
||||
} from '../mock/enriched_field_metadata/mock_enriched_field_metadata';
|
||||
import { TestProviders } from '../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../mock/test_providers/test_providers';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getCustomTableColumns', () => {
|
||||
|
@ -48,12 +48,12 @@ describe('helpers', () => {
|
|||
const indexFieldTypeRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{indexFieldTypeRender != null &&
|
||||
indexFieldTypeRender(someField.indexFieldType, someField)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('indexFieldType')).toHaveTextContent(someField.indexFieldType);
|
||||
|
@ -69,7 +69,7 @@ describe('helpers', () => {
|
|||
name: 'Field',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
|
@ -90,7 +90,7 @@ describe('helpers', () => {
|
|||
name: 'ECS description',
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -102,9 +102,9 @@ describe('helpers', () => {
|
|||
const typeRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>{typeRender != null && typeRender(eventCategory.type, eventCategory)}</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -125,12 +125,12 @@ describe('helpers', () => {
|
|||
const allowedValuesRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{allowedValuesRender != null &&
|
||||
allowedValuesRender(eventCategory.allowed_values, eventCategory)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -156,7 +156,7 @@ describe('helpers', () => {
|
|||
const allowedValuesRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{allowedValuesRender != null &&
|
||||
allowedValuesRender(
|
||||
|
@ -164,7 +164,7 @@ describe('helpers', () => {
|
|||
withUndefinedAllowedValues
|
||||
)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -185,12 +185,12 @@ describe('helpers', () => {
|
|||
const descriptionRender = columns[3].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{descriptionRender != null &&
|
||||
descriptionRender(eventCategory.description, eventCategory)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -215,7 +215,7 @@ describe('helpers', () => {
|
|||
name: 'Field',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'allowed_values',
|
||||
|
@ -236,7 +236,7 @@ describe('helpers', () => {
|
|||
name: 'ECS description',
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -248,12 +248,12 @@ describe('helpers', () => {
|
|||
const allowedValuesRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{allowedValuesRender != null &&
|
||||
allowedValuesRender(eventCategory.allowed_values, eventCategory)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -279,7 +279,7 @@ describe('helpers', () => {
|
|||
const allowedValuesRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{allowedValuesRender != null &&
|
||||
allowedValuesRender(
|
||||
|
@ -287,7 +287,7 @@ describe('helpers', () => {
|
|||
withUndefinedAllowedValues
|
||||
)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -308,7 +308,7 @@ describe('helpers', () => {
|
|||
const indexInvalidValuesRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{indexInvalidValuesRender != null &&
|
||||
indexInvalidValuesRender(
|
||||
|
@ -316,7 +316,7 @@ describe('helpers', () => {
|
|||
eventCategoryWithUnallowedValues
|
||||
)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -337,12 +337,12 @@ describe('helpers', () => {
|
|||
const indexInvalidValuesRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<>
|
||||
{indexInvalidValuesRender != null &&
|
||||
indexInvalidValuesRender(eventCategory.indexInvalidValues, eventCategory)}
|
||||
</>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export const getEcsCompliantTableColumns = (): Array<
|
|||
name: i18n.FIELD,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
|
@ -78,7 +78,7 @@ export const getEcsCompliantTableColumns = (): Array<
|
|||
render: (description: string) => <span data-test-subj="description">{description}</span>,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -90,7 +90,7 @@ export const getIncompatibleValuesTableColumns = (): Array<
|
|||
name: i18n.FIELD,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'allowed_values',
|
||||
|
@ -117,6 +117,6 @@ export const getIncompatibleValuesTableColumns = (): Array<
|
|||
name: i18n.ECS_DESCRIPTION,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
|
||||
import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../data_quality_panel/tabs/incompatible_tab/translations';
|
||||
import { eventCategory } from '../mock/enriched_field_metadata/mock_enriched_field_metadata';
|
||||
import { TestProviders } from '../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../mock/test_providers/test_providers';
|
||||
import { CompareFieldsTable } from '.';
|
||||
import { getIncompatibleMappingsTableColumns } from './get_incompatible_mappings_table_columns';
|
||||
|
||||
|
@ -18,13 +18,13 @@ describe('CompareFieldsTable', () => {
|
|||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<CompareFieldsTable
|
||||
enrichedFieldMetadata={[eventCategory]}
|
||||
getTableColumns={getIncompatibleMappingsTableColumns}
|
||||
title={INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE('foo')}
|
||||
/>
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { EMPTY_PLACEHOLDER } from '../helpers';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { UnallowedValueCount } from '../../types';
|
||||
import { IndexInvalidValues } from '.';
|
||||
|
||||
describe('IndexInvalidValues', () => {
|
||||
test('it renders a placeholder with the expected content when `indexInvalidValues` is empty', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<IndexInvalidValues indexInvalidValues={[]} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('emptyPlaceholder')).toHaveTextContent(EMPTY_PLACEHOLDER);
|
||||
|
@ -37,9 +37,9 @@ describe('IndexInvalidValues', () => {
|
|||
];
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<IndexInvalidValues indexInvalidValues={indexInvalidValues} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent(
|
||||
|
|
|
@ -6,7 +6,35 @@
|
|||
*/
|
||||
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { EcsFieldMetadata } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const EcsFlatTyped = EcsFlat as unknown as Record<string, EcsFieldMetadata>;
|
||||
export type EcsFlatTyped = typeof EcsFlatTyped;
|
||||
|
||||
export const ilmPhaseOptionsStatic: EuiComboBoxOptionOption[] = [
|
||||
{
|
||||
label: i18n.HOT,
|
||||
value: 'hot',
|
||||
},
|
||||
{
|
||||
label: i18n.WARM,
|
||||
value: 'warm',
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
label: i18n.COLD,
|
||||
value: 'cold',
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
label: i18n.FROZEN,
|
||||
value: 'frozen',
|
||||
},
|
||||
{
|
||||
label: i18n.UNMANAGED,
|
||||
value: 'unmanaged',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { UseIndicesCheckReturnValue } from '../../use_indices_check/types';
|
||||
|
||||
export const IndicesCheckContext = createContext<UseIndicesCheckReturnValue | null>(null);
|
||||
|
||||
export const useIndicesCheckContext = () => {
|
||||
const context = useContext(IndicesCheckContext);
|
||||
if (context == null) {
|
||||
throw new Error('useIndicesCheckContext must be used inside the IndicesCheckContextProvider.');
|
||||
}
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { UseResultsRollupReturnValue } from '../../use_results_rollup/types';
|
||||
|
||||
export const ResultsRollupContext = createContext<UseResultsRollupReturnValue | null>(null);
|
||||
|
||||
export const useResultsRollupContext = () => {
|
||||
const context = useContext(ResultsRollupContext);
|
||||
if (context == null) {
|
||||
throw new Error(
|
||||
'useResultsRollupContext must be used inside the ResultsRollupContextProvider.'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
|
||||
import { AddToNewCaseAction } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('AddToNewCaseAction', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render add to new case link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<AddToNewCaseAction markdownComment="test markdown" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('addToNewCase')).toHaveTextContent('Add to new case');
|
||||
});
|
||||
|
||||
describe('when ilm phases are not provided', () => {
|
||||
it('should render disabled add to new case link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ ilmPhases: [] }}>
|
||||
<AddToNewCaseAction markdownComment="test markdown" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('addToNewCase')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createAndReadCases() returns false', () => {
|
||||
it('should render disabled add to new case link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{ canUserCreateAndReadCases: () => false }}
|
||||
>
|
||||
<AddToNewCaseAction markdownComment="test markdown" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('addToNewCase')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clicking on add to new case link', () => {
|
||||
it('should open create case flyout with header content and provided markdown', () => {
|
||||
let headerContent: React.ReactNode = null;
|
||||
const openCreateCaseFlyout = jest.fn(({ headerContent: _headerContent }) => {
|
||||
headerContent = render(_headerContent).container;
|
||||
});
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ openCreateCaseFlyout }}>
|
||||
<AddToNewCaseAction markdownComment="test markdown" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('addToNewCase'));
|
||||
|
||||
expect(openCreateCaseFlyout).toHaveBeenCalledWith({
|
||||
comments: ['test markdown'],
|
||||
headerContent: expect.anything(),
|
||||
});
|
||||
|
||||
expect(headerContent).toContainHTML('<div>Create a data quality case</div>');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiIcon, EuiLink } from '@elastic/eui';
|
||||
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { useAddToNewCase } from '../../../use_add_to_new_case';
|
||||
import { StyledLinkText } from '../styles';
|
||||
import { ADD_TO_NEW_CASE } from '../../../translations';
|
||||
|
||||
interface Props {
|
||||
markdownComment: string;
|
||||
}
|
||||
|
||||
const AddToNewCaseActionComponent: React.FC<Props> = ({ markdownComment }) => {
|
||||
const { canUserCreateAndReadCases, openCreateCaseFlyout, ilmPhases } = useDataQualityContext();
|
||||
const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
openCreateCaseFlyout,
|
||||
});
|
||||
|
||||
const onClickAddToCase = useCallback(
|
||||
() => onAddToNewCase([markdownComment]),
|
||||
[markdownComment, onAddToNewCase]
|
||||
);
|
||||
|
||||
const addToNewCaseContextMenuOnClick = useCallback(() => {
|
||||
onClickAddToCase();
|
||||
}, [onClickAddToCase]);
|
||||
|
||||
const disableAll = ilmPhases.length === 0;
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
aria-label={ADD_TO_NEW_CASE}
|
||||
data-test-subj="addToNewCase"
|
||||
disabled={addToNewCaseDisabled || disableAll}
|
||||
onClick={addToNewCaseContextMenuOnClick}
|
||||
>
|
||||
<StyledLinkText>
|
||||
<EuiIcon type="listAdd" />
|
||||
{ADD_TO_NEW_CASE}
|
||||
</StyledLinkText>
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
AddToNewCaseActionComponent.displayName = 'AddToNewCaseActionComponent';
|
||||
|
||||
export const AddToNewCaseAction = React.memo(AddToNewCaseActionComponent);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
|
||||
import { ChatAction } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
|
||||
describe('ChatAction', () => {
|
||||
it('should render new chat link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<ChatAction markdownComment="test markdown" indexName="test-index" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('newChatLink')).toHaveTextContent('Ask Assistant');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { AssistantAvatar, NewChat } from '@kbn/elastic-assistant';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
DATA_QUALITY_DASHBOARD_CONVERSATION_ID,
|
||||
DATA_QUALITY_PROMPT_CONTEXT_PILL,
|
||||
DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP,
|
||||
DATA_QUALITY_SUGGESTED_USER_PROMPT,
|
||||
} from '../../../translations';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { ASK_ASSISTANT } from './translations';
|
||||
|
||||
const StyledLinkText = styled.span`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
markdownComment: string;
|
||||
indexName: string;
|
||||
}
|
||||
|
||||
const ChatActionComponent: FC<Props> = ({ indexName, markdownComment }) => {
|
||||
const { isAssistantEnabled } = useDataQualityContext();
|
||||
const getPromptContext = useCallback(async () => markdownComment, [markdownComment]);
|
||||
return (
|
||||
<NewChat
|
||||
asLink={true}
|
||||
category="data-quality-dashboard"
|
||||
conversationId={DATA_QUALITY_DASHBOARD_CONVERSATION_ID}
|
||||
description={DATA_QUALITY_PROMPT_CONTEXT_PILL(indexName)}
|
||||
getPromptContext={getPromptContext}
|
||||
suggestedUserPrompt={DATA_QUALITY_SUGGESTED_USER_PROMPT}
|
||||
tooltip={DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
iconType={null}
|
||||
>
|
||||
<StyledLinkText>
|
||||
<AssistantAvatar size="xs" />
|
||||
{ASK_ASSISTANT}
|
||||
</StyledLinkText>
|
||||
</NewChat>
|
||||
);
|
||||
};
|
||||
|
||||
ChatActionComponent.displayName = 'ChatActionComponent';
|
||||
|
||||
export const ChatAction = React.memo(ChatActionComponent);
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DATA_QUALITY_DASHBOARD_CONVERSATION_ID = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.incompatibleTab.dataQualityDashboardConversationId',
|
||||
export const ASK_ASSISTANT = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.actions.askAssistant',
|
||||
{
|
||||
defaultMessage: 'Data Quality dashboard',
|
||||
defaultMessage: 'Ask Assistant',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { copyToClipboard } from '@elastic/eui';
|
||||
|
||||
import { CopyToClipboardAction } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
||||
return {
|
||||
...original,
|
||||
copyToClipboard: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('CopyToClipboardAction', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render a copy to clipboard link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<CopyToClipboardAction markdownComment="test comment" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('copyToClipboard')).toHaveTextContent('Copy to clipboard');
|
||||
});
|
||||
|
||||
describe('when ilm phases are not provided', () => {
|
||||
it('should render disabled copy to clipboard link', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ ilmPhases: [] }}>
|
||||
<CopyToClipboardAction markdownComment="test comment" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('copyToClipboard')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when copy to clipboard is clicked', () => {
|
||||
it('should copy the markdown comment to the clipboard and add success toast', () => {
|
||||
const addSuccessToast = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ addSuccessToast }}>
|
||||
<CopyToClipboardAction markdownComment="test comment" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('copyToClipboard'));
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith('test comment');
|
||||
expect(addSuccessToast).toHaveBeenCalledWith({ title: 'Copied results to the clipboard' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiIcon, EuiLink, copyToClipboard } from '@elastic/eui';
|
||||
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { COPIED_RESULTS_TOAST_TITLE, COPY_TO_CLIPBOARD } from '../../../translations';
|
||||
import { StyledLinkText } from '../styles';
|
||||
|
||||
interface Props {
|
||||
markdownComment: string;
|
||||
}
|
||||
|
||||
const CopyToClipboardActionComponent: React.FC<Props> = ({ markdownComment }) => {
|
||||
const { addSuccessToast, ilmPhases } = useDataQualityContext();
|
||||
const onCopy = useCallback(() => {
|
||||
copyToClipboard(markdownComment);
|
||||
|
||||
addSuccessToast({
|
||||
title: COPIED_RESULTS_TOAST_TITLE,
|
||||
});
|
||||
}, [addSuccessToast, markdownComment]);
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
aria-label={COPY_TO_CLIPBOARD}
|
||||
data-test-subj="copyToClipboard"
|
||||
disabled={ilmPhases.length === 0}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<StyledLinkText>
|
||||
<EuiIcon type="copyClipboard" />
|
||||
{COPY_TO_CLIPBOARD}
|
||||
</StyledLinkText>
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
CopyToClipboardActionComponent.displayName = 'CopyToClipboardActionComponent';
|
||||
|
||||
export const CopyToClipboardAction = React.memo(CopyToClipboardActionComponent);
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { Actions } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
|
||||
describe('Actions', () => {
|
||||
it('renders nothing by default', () => {
|
||||
const { container } = render(<Actions markdownComment="some markdown" />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
describe('given showAddToNewCaseAction is true', () => {
|
||||
it('renders AddToNewCaseAction', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Actions markdownComment="some markdown" showAddToNewCaseAction />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('addToNewCase')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('given showCopyToClipboardAction is true', () => {
|
||||
it('renders CopyToClipboardAction', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Actions markdownComment="some markdown" showCopyToClipboardAction />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('copyToClipboard')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('given showChatAction is true and indexName is present', () => {
|
||||
it('renders ChatAction', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Actions markdownComment="some markdown" showChatAction indexName="some-index" />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('newChatLink')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { ChatAction } from './chat';
|
||||
import { CopyToClipboardAction } from './copy_to_clipboard';
|
||||
import { AddToNewCaseAction } from './add_to_new_case';
|
||||
|
||||
export interface Props {
|
||||
markdownComment: string;
|
||||
indexName?: string;
|
||||
showAddToNewCaseAction?: boolean;
|
||||
showCopyToClipboardAction?: boolean;
|
||||
showChatAction?: boolean;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<Props> = ({
|
||||
showAddToNewCaseAction,
|
||||
showCopyToClipboardAction,
|
||||
showChatAction,
|
||||
markdownComment,
|
||||
indexName,
|
||||
}) => {
|
||||
if (!showAddToNewCaseAction && !showCopyToClipboardAction && !showChatAction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="actions">
|
||||
{showAddToNewCaseAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddToNewCaseAction markdownComment={markdownComment} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{showCopyToClipboardAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
{' '}
|
||||
<CopyToClipboardAction markdownComment={markdownComment} />{' '}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{showChatAction && indexName && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ChatAction indexName={indexName} markdownComment={markdownComment} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsComponent.displayName = 'ActionsComponent';
|
||||
|
||||
export const Actions = React.memo(ActionsComponent);
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PartitionedFieldMetadata } from '../types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const allMetadataIsEmpty = (partitionedFieldMetadata: PartitionedFieldMetadata): boolean =>
|
||||
partitionedFieldMetadata.all.length === 0;
|
||||
export const StyledLinkText = styled.span`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
|
@ -5,65 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { EMPTY_STAT } from '../../../helpers';
|
||||
import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../../types';
|
||||
import { Props, DataQualityDetails } from '.';
|
||||
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
|
||||
|
||||
const defaultNumberFormat = '0,0.[000]';
|
||||
const formatNumber = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { DataQualityDetails } from '.';
|
||||
|
||||
const ilmPhases = ['hot', 'warm', 'unmanaged'];
|
||||
const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*'];
|
||||
|
||||
const patternRollups: Record<string, PatternRollup> = {
|
||||
'.alerts-security.alerts-default': alertIndexWithAllResults,
|
||||
'auditbeat-*': auditbeatWithAllResults,
|
||||
'packetbeat-*': packetbeatNoResults,
|
||||
};
|
||||
|
||||
const patternIndexNames: Record<string, string[]> = {
|
||||
'auditbeat-*': [
|
||||
'.ds-auditbeat-8.6.1-2023.02.07-000001',
|
||||
'auditbeat-custom-empty-index-1',
|
||||
'auditbeat-custom-index-1',
|
||||
],
|
||||
'.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'],
|
||||
'packetbeat-*': [
|
||||
'.ds-packetbeat-8.5.3-2023.02.04-000001',
|
||||
'.ds-packetbeat-8.6.1-2023.02.04-000001',
|
||||
],
|
||||
};
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: jest.fn(),
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick: jest.fn(),
|
||||
ilmPhases,
|
||||
isAssistantEnabled: true,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
baseTheme: DARK_THEME,
|
||||
updatePatternIndexNames: jest.fn(),
|
||||
updatePatternRollup: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DataQualityDetails', () => {
|
||||
describe('when ILM phases are provided', () => {
|
||||
|
@ -71,9 +22,11 @@ describe('DataQualityDetails', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<DataQualityDetails {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ ilmPhases }}>
|
||||
<DataQualityDetails />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {}); // wait for PatternComponent state updates
|
||||
|
@ -93,9 +46,11 @@ describe('DataQualityDetails', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<DataQualityDetails {...defaultProps} ilmPhases={[]} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ ilmPhases: [] }}>
|
||||
<DataQualityDetails />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,93 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FlameElementEvent,
|
||||
HeatmapElementEvent,
|
||||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { IlmPhasesEmptyPrompt } from '../../../ilm_phases_empty_prompt';
|
||||
import { IndicesDetails } from './indices_details';
|
||||
import { StorageDetails } from './storage_details';
|
||||
import { PatternRollup, SelectedIndex } from '../../../types';
|
||||
import { SelectedIndex } from '../../../types';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
baseTheme: Theme;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
endDate?: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
getGroupByFieldsOnClick: (
|
||||
elements: Array<
|
||||
| FlameElementEvent
|
||||
| HeatmapElementEvent
|
||||
| MetricElementEvent
|
||||
| PartitionElementEvent
|
||||
| WordCloudElementEvent
|
||||
| XYChartElementEvent
|
||||
>
|
||||
) => {
|
||||
groupByField0: string;
|
||||
groupByField1: string;
|
||||
};
|
||||
ilmPhases: string[];
|
||||
isAssistantEnabled: boolean;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patternIndexNames: Record<string, string[]>;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
startDate?: string | null;
|
||||
theme?: PartialTheme;
|
||||
updatePatternIndexNames: ({
|
||||
indexNames,
|
||||
pattern,
|
||||
}: {
|
||||
indexNames: string[];
|
||||
pattern: string;
|
||||
}) => void;
|
||||
updatePatternRollup: (patternRollup: PatternRollup) => void;
|
||||
}
|
||||
const DataQualityDetailsComponent: React.FC = () => {
|
||||
const { isILMAvailable, ilmPhases } = useDataQualityContext();
|
||||
const [chartSelectedIndex, setChartSelectedIndex] = useState<SelectedIndex | null>(null);
|
||||
|
||||
const DataQualityDetailsComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhases,
|
||||
isAssistantEnabled,
|
||||
openCreateCaseFlyout,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
updatePatternIndexNames,
|
||||
updatePatternRollup,
|
||||
}) => {
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const [selectedIndex, setSelectedIndex] = useState<SelectedIndex | null>(null);
|
||||
|
||||
const onIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => {
|
||||
setSelectedIndex({ indexName, pattern });
|
||||
const handleChartsIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => {
|
||||
setChartSelectedIndex({ indexName, pattern });
|
||||
}, []);
|
||||
|
||||
if (isILMAvailable && ilmPhases.length === 0) {
|
||||
|
@ -100,37 +27,10 @@ const DataQualityDetailsComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<StorageDetails
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
onIndexSelected={onIndexSelected}
|
||||
patterns={patterns}
|
||||
theme={theme}
|
||||
baseTheme={baseTheme}
|
||||
patternRollups={patternRollups}
|
||||
/>
|
||||
|
||||
<StorageDetails onIndexSelected={handleChartsIndexSelected} />
|
||||
<IndicesDetails
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
endDate={endDate}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
|
||||
ilmPhases={ilmPhases}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
patterns={patterns}
|
||||
theme={theme}
|
||||
baseTheme={baseTheme}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patternRollups={patternRollups}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
startDate={startDate}
|
||||
updatePatternIndexNames={updatePatternIndexNames}
|
||||
updatePatternRollup={updatePatternRollup}
|
||||
chartSelectedIndex={chartSelectedIndex}
|
||||
setChartSelectedIndex={setChartSelectedIndex}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
@ -14,7 +13,10 @@ import { EMPTY_STAT } from '../../../../helpers';
|
|||
import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../../../types';
|
||||
import { Props, IndicesDetails } from '.';
|
||||
|
||||
|
@ -49,22 +51,8 @@ const patternIndexNames: Record<string, string[]> = {
|
|||
};
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: jest.fn(),
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick: jest.fn(),
|
||||
ilmPhases,
|
||||
isAssistantEnabled: true,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
selectedIndex: null,
|
||||
setSelectedIndex: jest.fn(),
|
||||
baseTheme: DARK_THEME,
|
||||
updatePatternIndexNames: jest.fn(),
|
||||
updatePatternRollup: jest.fn(),
|
||||
chartSelectedIndex: null,
|
||||
setChartSelectedIndex: jest.fn(),
|
||||
};
|
||||
|
||||
describe('IndicesDetails', () => {
|
||||
|
@ -72,9 +60,14 @@ describe('IndicesDetails', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndicesDetails {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{ ilmPhases, patterns, formatBytes, formatNumber }}
|
||||
resultsRollupContextProps={{ patternRollups, patternIndexNames }}
|
||||
>
|
||||
<IndicesDetails {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {});
|
||||
|
@ -87,10 +80,4 @@ describe('IndicesDetails', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering spacers', () => {
|
||||
test('it renders the expected number of spacers', () => {
|
||||
expect(screen.getAllByTestId('bodyPatternSpacer')).toHaveLength(patterns.length - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,118 +5,51 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FlameElementEvent,
|
||||
HeatmapElementEvent,
|
||||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
import { EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useResultsRollupContext } from '../../../../contexts/results_rollup_context';
|
||||
import { Pattern } from '../../../pattern';
|
||||
import { PatternRollup, SelectedIndex } from '../../../../types';
|
||||
import { SelectedIndex } from '../../../../types';
|
||||
import { useDataQualityContext } from '../../../data_quality_context';
|
||||
|
||||
const StyledPatternWrapperFlexItem = styled(EuiFlexItem)`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSize};
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
endDate?: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
getGroupByFieldsOnClick: (
|
||||
elements: Array<
|
||||
| FlameElementEvent
|
||||
| HeatmapElementEvent
|
||||
| MetricElementEvent
|
||||
| PartitionElementEvent
|
||||
| WordCloudElementEvent
|
||||
| XYChartElementEvent
|
||||
>
|
||||
) => {
|
||||
groupByField0: string;
|
||||
groupByField1: string;
|
||||
};
|
||||
ilmPhases: string[];
|
||||
isAssistantEnabled: boolean;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patternIndexNames: Record<string, string[]>;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
selectedIndex: SelectedIndex | null;
|
||||
setSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
|
||||
startDate?: string | null;
|
||||
theme?: PartialTheme;
|
||||
baseTheme: Theme;
|
||||
updatePatternIndexNames: ({
|
||||
indexNames,
|
||||
pattern,
|
||||
}: {
|
||||
indexNames: string[];
|
||||
pattern: string;
|
||||
}) => void;
|
||||
updatePatternRollup: (patternRollup: PatternRollup) => void;
|
||||
chartSelectedIndex: SelectedIndex | null;
|
||||
setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
|
||||
}
|
||||
|
||||
const IndicesDetailsComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhases,
|
||||
isAssistantEnabled,
|
||||
openCreateCaseFlyout,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
updatePatternIndexNames,
|
||||
updatePatternRollup,
|
||||
}) => (
|
||||
<div data-test-subj="indicesDetails">
|
||||
{patterns.map((pattern, i) => (
|
||||
<EuiFlexItem grow={false} key={pattern}>
|
||||
<Pattern
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
endDate={endDate}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
|
||||
ilmPhases={ilmPhases}
|
||||
indexNames={patternIndexNames[pattern]}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
pattern={pattern}
|
||||
patternRollup={patternRollups[pattern]}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
startDate={startDate}
|
||||
theme={theme}
|
||||
baseTheme={baseTheme}
|
||||
updatePatternIndexNames={updatePatternIndexNames}
|
||||
updatePatternRollup={updatePatternRollup}
|
||||
/>
|
||||
{patterns[i + 1] && <EuiSpacer data-test-subj="bodyPatternSpacer" size="s" />}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
chartSelectedIndex,
|
||||
setChartSelectedIndex,
|
||||
}) => {
|
||||
const { patternRollups, patternIndexNames } = useResultsRollupContext();
|
||||
const { patterns } = useDataQualityContext();
|
||||
|
||||
return (
|
||||
<div data-test-subj="indicesDetails">
|
||||
{patterns.map((pattern) => (
|
||||
<StyledPatternWrapperFlexItem grow={false} key={pattern}>
|
||||
<Pattern
|
||||
indexNames={patternIndexNames[pattern]}
|
||||
pattern={pattern}
|
||||
patternRollup={patternRollups[pattern]}
|
||||
chartSelectedIndex={chartSelectedIndex}
|
||||
setChartSelectedIndex={setChartSelectedIndex}
|
||||
/>
|
||||
</StyledPatternWrapperFlexItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
IndicesDetailsComponent.displayName = 'IndicesDetailsComponent';
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
@ -14,7 +13,10 @@ import { EMPTY_STAT } from '../../../../helpers';
|
|||
import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../../../types';
|
||||
import { Props, StorageDetails } from '.';
|
||||
|
||||
|
@ -38,13 +40,7 @@ const patternRollups: Record<string, PatternRollup> = {
|
|||
const onIndexSelected = jest.fn();
|
||||
|
||||
const defaultProps: Props = {
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
onIndexSelected,
|
||||
patternRollups,
|
||||
patterns,
|
||||
baseTheme: DARK_THEME,
|
||||
};
|
||||
|
||||
describe('StorageDetails', () => {
|
||||
|
@ -52,9 +48,14 @@ describe('StorageDetails', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<StorageDetails {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{ ilmPhases, patterns, formatBytes, formatNumber }}
|
||||
resultsRollupContextProps={{ patternRollups }}
|
||||
>
|
||||
<StorageDetails {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,38 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PartialTheme, Theme } from '@elastic/charts';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useResultsRollupContext } from '../../../../contexts/results_rollup_context';
|
||||
import { getFlattenedBuckets } from './helpers';
|
||||
import { StorageTreemap } from '../../../storage_treemap';
|
||||
import { DEFAULT_MAX_CHART_HEIGHT, StorageTreemapContainer } from '../../../tabs/styles';
|
||||
import { PatternRollup, SelectedIndex } from '../../../../types';
|
||||
import { DEFAULT_MAX_CHART_HEIGHT } from '../../../tabs/styles';
|
||||
import { SelectedIndex } from '../../../../types';
|
||||
import { useDataQualityContext } from '../../../data_quality_context';
|
||||
import { DOCS_UNIT } from './translations';
|
||||
|
||||
export interface Props {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
ilmPhases: string[];
|
||||
onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
theme?: PartialTheme;
|
||||
baseTheme: Theme;
|
||||
}
|
||||
|
||||
const StorageDetailsComponent: React.FC<Props> = ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
onIndexSelected,
|
||||
patternRollups,
|
||||
patterns,
|
||||
theme,
|
||||
baseTheme,
|
||||
}) => {
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const StorageDetailsComponent: React.FC<Props> = ({ onIndexSelected }) => {
|
||||
const { patternRollups } = useResultsRollupContext();
|
||||
const { isILMAvailable, ilmPhases, formatBytes, formatNumber } = useDataQualityContext();
|
||||
|
||||
const flattenedBuckets = useMemo(
|
||||
() =>
|
||||
|
@ -55,19 +40,16 @@ const StorageDetailsComponent: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<StorageTreemapContainer data-test-subj="storageDetails">
|
||||
<div data-test-subj="storageDetails">
|
||||
<StorageTreemap
|
||||
accessor={accessor}
|
||||
baseTheme={baseTheme}
|
||||
flattenedBuckets={flattenedBuckets}
|
||||
maxChartHeight={DEFAULT_MAX_CHART_HEIGHT}
|
||||
onIndexSelected={onIndexSelected}
|
||||
patternRollups={patternRollups}
|
||||
patterns={patterns}
|
||||
theme={theme}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
</StorageTreemapContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,44 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { EMPTY_STAT } from '../../helpers';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import { Body } from '.';
|
||||
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
|
||||
|
||||
const defaultNumberFormat = '0,0.[000]';
|
||||
const formatNumber = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
|
||||
const ilmPhases: string[] = ['hot', 'warm', 'unmanaged'];
|
||||
|
||||
describe('IndexInvalidValues', () => {
|
||||
test('it renders the data quality summary', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addSuccessToast={jest.fn()}
|
||||
canUserCreateAndReadCases={jest.fn()}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={jest.fn()}
|
||||
ilmPhases={[]}
|
||||
isAssistantEnabled={true}
|
||||
lastChecked={''}
|
||||
openCreateCaseFlyout={jest.fn()}
|
||||
patterns={[]}
|
||||
setLastChecked={jest.fn()}
|
||||
baseTheme={DARK_THEME}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Body />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -53,57 +32,20 @@ describe('IndexInvalidValues', () => {
|
|||
describe('patterns', () => {
|
||||
const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'logs-*', 'packetbeat-*'];
|
||||
|
||||
patterns.forEach((pattern) => {
|
||||
test(`it renders the '${pattern}' pattern`, async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addSuccessToast={jest.fn()}
|
||||
canUserCreateAndReadCases={jest.fn()}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={jest.fn()}
|
||||
ilmPhases={ilmPhases}
|
||||
isAssistantEnabled={true}
|
||||
lastChecked={''}
|
||||
openCreateCaseFlyout={jest.fn()}
|
||||
patterns={patterns}
|
||||
setLastChecked={jest.fn()}
|
||||
baseTheme={DARK_THEME}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
test(`it renders the '${patterns.join(', ')}' patterns`, async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ patterns }}>
|
||||
<Body />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
for (const pattern of patterns) {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders the expected number of spacers', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addSuccessToast={jest.fn()}
|
||||
canUserCreateAndReadCases={jest.fn()}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={jest.fn()}
|
||||
ilmPhases={ilmPhases}
|
||||
isAssistantEnabled={true}
|
||||
lastChecked={''}
|
||||
openCreateCaseFlyout={jest.fn()}
|
||||
patterns={patterns}
|
||||
setLastChecked={jest.fn()}
|
||||
baseTheme={DARK_THEME}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const items = await screen.findAllByTestId('bodyPatternSpacer');
|
||||
await waitFor(() => {
|
||||
expect(items).toHaveLength(patterns.length - 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,137 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FlameElementEvent,
|
||||
HeatmapElementEvent,
|
||||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { DataQualityDetails } from './data_quality_details';
|
||||
import { DataQualitySummary } from '../data_quality_summary';
|
||||
import { useResultsRollup } from '../../use_results_rollup';
|
||||
|
||||
interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
baseTheme: Theme;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
endDate?: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
getGroupByFieldsOnClick: (
|
||||
elements: Array<
|
||||
| FlameElementEvent
|
||||
| HeatmapElementEvent
|
||||
| MetricElementEvent
|
||||
| PartitionElementEvent
|
||||
| WordCloudElementEvent
|
||||
| XYChartElementEvent
|
||||
>
|
||||
) => {
|
||||
groupByField0: string;
|
||||
groupByField1: string;
|
||||
};
|
||||
ilmPhases: string[];
|
||||
isAssistantEnabled: boolean;
|
||||
lastChecked: string;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patterns: string[];
|
||||
setLastChecked: (lastChecked: string) => void;
|
||||
startDate?: string | null;
|
||||
theme?: PartialTheme;
|
||||
}
|
||||
|
||||
const BodyComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhases,
|
||||
isAssistantEnabled,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
}) => {
|
||||
const {
|
||||
onCheckCompleted,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
updatePatternIndexNames,
|
||||
updatePatternRollup,
|
||||
} = useResultsRollup({
|
||||
ilmPhases,
|
||||
patterns,
|
||||
});
|
||||
|
||||
const BodyComponent: React.FC = () => {
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="body" direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DataQualitySummary
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
lastChecked={lastChecked}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patternRollups={patternRollups}
|
||||
patterns={patterns}
|
||||
setLastChecked={setLastChecked}
|
||||
totalDocsCount={totalDocsCount}
|
||||
totalIncompatible={totalIncompatible}
|
||||
totalIndices={totalIndices}
|
||||
totalIndicesChecked={totalIndicesChecked}
|
||||
totalSizeInBytes={totalSizeInBytes}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
/>
|
||||
<DataQualitySummary />
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<DataQualityDetails
|
||||
addSuccessToast={addSuccessToast}
|
||||
baseTheme={baseTheme}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
endDate={endDate}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
|
||||
ilmPhases={ilmPhases}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
patterns={patterns}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patternRollups={patternRollups}
|
||||
startDate={startDate}
|
||||
theme={theme}
|
||||
updatePatternIndexNames={updatePatternIndexNames}
|
||||
updatePatternRollup={updatePatternRollup}
|
||||
/>
|
||||
<DataQualityDetails />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Theme } from '@elastic/charts';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
|
@ -25,6 +26,45 @@ const ContextWrapper: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
|||
telemetryEvents={mockTelemetryEvents}
|
||||
isILMAvailable={true}
|
||||
toasts={toasts}
|
||||
addSuccessToast={jest.fn()}
|
||||
canUserCreateAndReadCases={jest.fn(() => true)}
|
||||
endDate={null}
|
||||
formatBytes={jest.fn()}
|
||||
formatNumber={jest.fn()}
|
||||
isAssistantEnabled={true}
|
||||
lastChecked={'2023-03-28T22:27:28.159Z'}
|
||||
openCreateCaseFlyout={jest.fn()}
|
||||
patterns={['auditbeat-*']}
|
||||
setLastChecked={jest.fn()}
|
||||
startDate={null}
|
||||
theme={{
|
||||
background: {
|
||||
color: '#000',
|
||||
},
|
||||
}}
|
||||
baseTheme={
|
||||
{
|
||||
background: {
|
||||
color: '#000',
|
||||
},
|
||||
} as Theme
|
||||
}
|
||||
ilmPhases={['hot', 'warm', 'unmanaged']}
|
||||
selectedIlmPhaseOptions={[
|
||||
{
|
||||
label: 'Hot',
|
||||
value: 'hot',
|
||||
},
|
||||
{
|
||||
label: 'Warm',
|
||||
value: 'warm',
|
||||
},
|
||||
{
|
||||
label: 'Unmanaged',
|
||||
value: 'unmanaged',
|
||||
},
|
||||
]}
|
||||
setSelectedIlmPhaseOptions={jest.fn()}
|
||||
>
|
||||
{children}
|
||||
</DataQualityProvider>
|
||||
|
@ -43,9 +83,9 @@ describe('DataQualityContext', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it should return the httpFetch function', async () => {
|
||||
test('it should return the httpFetch function', () => {
|
||||
const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper });
|
||||
const httpFetch = await result.current.httpFetch;
|
||||
const httpFetch = result.current.httpFetch;
|
||||
|
||||
const path = '/path/to/resource';
|
||||
httpFetch(path);
|
||||
|
@ -53,16 +93,16 @@ describe('DataQualityContext', () => {
|
|||
expect(mockHttpFetch).toBeCalledWith(path);
|
||||
});
|
||||
|
||||
test('it should return the telemetry events', async () => {
|
||||
test('it should return the telemetry events', () => {
|
||||
const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper });
|
||||
const telemetryEvents = await result.current.telemetryEvents;
|
||||
const telemetryEvents = result.current.telemetryEvents;
|
||||
|
||||
expect(telemetryEvents).toEqual(mockTelemetryEvents);
|
||||
});
|
||||
|
||||
test('it should return the isILMAvailable param', async () => {
|
||||
const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper });
|
||||
const isILMAvailable = await result.current.isILMAvailable;
|
||||
const isILMAvailable = result.current.isILMAvailable;
|
||||
|
||||
expect(isILMAvailable).toEqual(true);
|
||||
});
|
||||
|
|
|
@ -9,13 +9,38 @@ import React, { useMemo } from 'react';
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import type { HttpHandler } from '@kbn/core-http-browser';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { PartialTheme, Theme } from '@elastic/charts';
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import type { TelemetryEvents } from '../../types';
|
||||
|
||||
interface DataQualityProviderProps {
|
||||
export interface DataQualityProviderProps {
|
||||
httpFetch: HttpHandler;
|
||||
isILMAvailable: boolean;
|
||||
telemetryEvents: TelemetryEvents;
|
||||
toasts: IToasts;
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
baseTheme: Theme;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
endDate?: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
isAssistantEnabled: boolean;
|
||||
lastChecked: string;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patterns: string[];
|
||||
setLastChecked: (lastChecked: string) => void;
|
||||
startDate?: string | null;
|
||||
theme?: PartialTheme;
|
||||
ilmPhases: string[];
|
||||
selectedIlmPhaseOptions: EuiComboBoxOptionOption[];
|
||||
setSelectedIlmPhaseOptions: (options: EuiComboBoxOptionOption[]) => void;
|
||||
}
|
||||
|
||||
const DataQualityContext = React.createContext<DataQualityProviderProps | undefined>(undefined);
|
||||
|
@ -26,6 +51,22 @@ export const DataQualityProvider: React.FC<PropsWithChildren<DataQualityProvider
|
|||
toasts,
|
||||
isILMAvailable,
|
||||
telemetryEvents,
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isAssistantEnabled,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
ilmPhases,
|
||||
selectedIlmPhaseOptions,
|
||||
setSelectedIlmPhaseOptions,
|
||||
}) => {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
@ -33,8 +74,45 @@ export const DataQualityProvider: React.FC<PropsWithChildren<DataQualityProvider
|
|||
toasts,
|
||||
isILMAvailable,
|
||||
telemetryEvents,
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isAssistantEnabled,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
ilmPhases,
|
||||
selectedIlmPhaseOptions,
|
||||
setSelectedIlmPhaseOptions,
|
||||
}),
|
||||
[httpFetch, toasts, isILMAvailable, telemetryEvents]
|
||||
[
|
||||
httpFetch,
|
||||
toasts,
|
||||
isILMAvailable,
|
||||
telemetryEvents,
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isAssistantEnabled,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
ilmPhases,
|
||||
selectedIlmPhaseOptions,
|
||||
setSelectedIlmPhaseOptions,
|
||||
]
|
||||
);
|
||||
|
||||
return <DataQualityContext.Provider value={value}>{children}</DataQualityContext.Provider>;
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
import { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { IndexToCheck } from '../../../types';
|
||||
import { CheckStatus, EMPTY_LAST_CHECKED_DATE } from '.';
|
||||
|
||||
|
@ -23,17 +26,16 @@ describe('CheckStatus', () => {
|
|||
describe('when `indexToCheck` is not null', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
lastChecked={''}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '' }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -65,17 +67,16 @@ describe('CheckStatus', () => {
|
|||
describe('when `indexToCheck` is null', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={null}
|
||||
lastChecked={''}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '' }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={null}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -102,17 +103,16 @@ describe('CheckStatus', () => {
|
|||
];
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={errorSummary}
|
||||
indexToCheck={null}
|
||||
lastChecked={''}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '' }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={errorSummary}
|
||||
indexToCheck={null}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('errorsPopover')).toBeInTheDocument();
|
||||
|
@ -120,17 +120,16 @@ describe('CheckStatus', () => {
|
|||
|
||||
test('it does NOT render the errors popover when errors have NOT occurred', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]} // <-- no errors
|
||||
indexToCheck={null}
|
||||
lastChecked={''}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '' }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]} // <-- no errors
|
||||
indexToCheck={null}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('errorsPopover')).not.toBeInTheDocument();
|
||||
|
@ -144,17 +143,16 @@ describe('CheckStatus', () => {
|
|||
const setLastChecked = jest.fn();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
lastChecked={''}
|
||||
setLastChecked={setLastChecked}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '', setLastChecked }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(setLastChecked).toBeCalledWith(date);
|
||||
|
@ -167,17 +165,16 @@ describe('CheckStatus', () => {
|
|||
jest.setSystemTime(new Date(date));
|
||||
|
||||
const { rerender } = render(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
lastChecked={''}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked: '' }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={indexToCheck}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
// re-render with an updated `lastChecked`
|
||||
|
@ -188,17 +185,16 @@ describe('CheckStatus', () => {
|
|||
});
|
||||
|
||||
rerender(
|
||||
<TestProviders>
|
||||
<CheckStatus
|
||||
addSuccessToast={jest.fn()}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={null} // <-- also updated
|
||||
lastChecked={lastChecked}
|
||||
setLastChecked={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ lastChecked }}>
|
||||
<CheckStatus
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={[]}
|
||||
indexToCheck={null} // <-- also updated
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
|
|
|
@ -12,28 +12,24 @@ import moment from 'moment';
|
|||
import { ErrorsPopover } from '../errors_popover';
|
||||
import * as i18n from '../../../translations';
|
||||
import type { ErrorSummary, IndexToCheck } from '../../../types';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
|
||||
export const EMPTY_LAST_CHECKED_DATE = '--';
|
||||
|
||||
interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
checkAllIndiciesChecked: number;
|
||||
checkAllTotalIndiciesToCheck: number;
|
||||
errorSummary: ErrorSummary[];
|
||||
indexToCheck: IndexToCheck | null;
|
||||
lastChecked: string;
|
||||
setLastChecked: (lastChecked: string) => void;
|
||||
}
|
||||
|
||||
const CheckStatusComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
checkAllIndiciesChecked,
|
||||
checkAllTotalIndiciesToCheck,
|
||||
errorSummary,
|
||||
indexToCheck,
|
||||
lastChecked,
|
||||
setLastChecked,
|
||||
}) => {
|
||||
const { addSuccessToast, lastChecked, setLastChecked } = useDataQualityContext();
|
||||
const [formattedDate, setFormattedDate] = useState<string>(EMPTY_LAST_CHECKED_DATE);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { ErrorsPopover } from '.';
|
||||
|
||||
const mockCopyToClipboard = jest.fn((value) => true);
|
||||
|
@ -36,9 +36,9 @@ describe('ErrorsPopover', () => {
|
|||
|
||||
test('it disables the view errors button when `errorSummary` is empty', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorsPopover addSuccessToast={jest.fn()} errorSummary={[]} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('viewErrors')).toBeDisabled();
|
||||
|
@ -46,9 +46,9 @@ describe('ErrorsPopover', () => {
|
|||
|
||||
test('it enables the view errors button when `errorSummary` is NOT empty', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorsPopover addSuccessToast={jest.fn()} errorSummary={errorSummary} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('viewErrors')).not.toBeDisabled();
|
||||
|
@ -61,9 +61,9 @@ describe('ErrorsPopover', () => {
|
|||
jest.resetAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorsPopover addSuccessToast={addSuccessToast} errorSummary={errorSummary} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const viewErrorsButton = screen.getByTestId('viewErrors');
|
||||
|
|
|
@ -10,7 +10,7 @@ import { omit } from 'lodash/fp';
|
|||
import React from 'react';
|
||||
|
||||
import { getErrorsViewerTableColumns } from './helpers';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { ErrorSummary } from '../../../types';
|
||||
|
||||
const errorSummary: ErrorSummary[] = [
|
||||
|
@ -67,9 +67,9 @@ describe('helpers', () => {
|
|||
const indexNameRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexNameRender != null && indexNameRender(hasIndexName.indexName, hasIndexName)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -88,9 +88,9 @@ describe('helpers', () => {
|
|||
const indexNameRender = columns[1].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexNameRender != null && indexNameRender(noIndexName.indexName, noIndexName)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -110,9 +110,9 @@ describe('helpers', () => {
|
|||
const indexNameRender = columns[2].render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexNameRender != null && indexNameRender(hasIndexName.error, hasIndexName)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('error')).toHaveTextContent(hasIndexName.error);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { ERROR, INDEX, PATTERN } from './translations';
|
||||
import { ErrorSummary } from '../../../types';
|
||||
import { ErrorsViewer } from '.';
|
||||
|
@ -51,9 +51,9 @@ describe('ErrorsViewer', () => {
|
|||
expectedColumns.forEach(({ id, expected }, i) => {
|
||||
test(`it renders the expected '${id}' column header`, () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorsViewer errorSummary={[]} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(`tableHeaderCell_${id}_${i}`)).toHaveTextContent(expected);
|
||||
|
@ -62,9 +62,9 @@ describe('ErrorsViewer', () => {
|
|||
|
||||
test(`it renders the expected the errors`, () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorsViewer errorSummary={errorSummary} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { IlmPhaseFilter } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
COLD_DESCRIPTION,
|
||||
FROZEN_DESCRIPTION,
|
||||
HOT_DESCRIPTION,
|
||||
INDEX_LIFECYCLE_MANAGEMENT_PHASES,
|
||||
UNMANAGED_DESCRIPTION,
|
||||
WARM_DESCRIPTION,
|
||||
} from '../../../translations';
|
||||
|
||||
describe('IlmPhaseFilter', () => {
|
||||
it('renders combobox with ilmPhase label and preselected hot, warm, unmanaged options', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IlmPhaseFilter />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
expect(screen.getByTestId('selectIlmPhases')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('ILM phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('hot')).toBeInTheDocument();
|
||||
expect(screen.getByText('warm')).toBeInTheDocument();
|
||||
expect(screen.getByText('unmanaged')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not preselect disabled cold, frozen options', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IlmPhaseFilter />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
expect(screen.queryByText('cold')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('frozen')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when dropdown opened', () => {
|
||||
it('shows remaining disabled options', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IlmPhaseFilter />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
const searchInput = screen.getByTestId('comboBoxSearchInput');
|
||||
|
||||
userEvent.click(searchInput);
|
||||
|
||||
expect(screen.getByTitle('frozen')).toHaveAttribute('role', 'option');
|
||||
expect(screen.getByTitle('frozen')).toBeDisabled();
|
||||
expect(screen.getByTitle('cold')).toHaveAttribute('role', 'option');
|
||||
expect(screen.getByTitle('cold')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when hovering over search input', () => {
|
||||
it('shows a tooltip with the ilm check description', async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IlmPhaseFilter />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
userEvent.hover(screen.getByTestId('comboBoxSearchInput'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(INDEX_LIFECYCLE_MANAGEMENT_PHASES)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when hovering over options in dropdown', () => {
|
||||
describe.each([
|
||||
{ option: 'hot', tooltipDescription: HOT_DESCRIPTION },
|
||||
{ option: 'warm', tooltipDescription: WARM_DESCRIPTION },
|
||||
{ option: 'unmanaged', tooltipDescription: UNMANAGED_DESCRIPTION },
|
||||
{
|
||||
option: 'cold',
|
||||
tooltipDescription: COLD_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
option: 'frozen',
|
||||
tooltipDescription: FROZEN_DESCRIPTION,
|
||||
},
|
||||
])('when hovering over $option option', ({ option, tooltipDescription }) => {
|
||||
it(`shows a tooltip with the ${option} description`, async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
selectedIlmPhaseOptions: [],
|
||||
}}
|
||||
>
|
||||
<IlmPhaseFilter />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText('Select one or more ILM phases')).toBeInTheDocument();
|
||||
|
||||
const searchInput = screen.getByTestId('comboBoxSearchInput');
|
||||
userEvent.click(searchInput);
|
||||
userEvent.hover(screen.getByText(option.toLowerCase()), undefined, {
|
||||
skipPointerEventsCheck: true,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(tooltipDescription)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormLabel,
|
||||
EuiToolTip,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ilmPhaseOptionsStatic } from '../../../constants';
|
||||
import { getIlmPhaseDescription } from '../../../helpers';
|
||||
import {
|
||||
ILM_PHASE,
|
||||
INDEX_LIFECYCLE_MANAGEMENT_PHASES,
|
||||
SELECT_ONE_OR_MORE_ILM_PHASES,
|
||||
} from '../../../translations';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { StyledFormControlLayout, StyledOption, StyledOptionLabel } from './styles';
|
||||
|
||||
const renderOption = (
|
||||
option: EuiComboBoxOptionOption<string | number | string[] | undefined>
|
||||
): React.ReactNode => (
|
||||
<EuiToolTip content={`${option.label}: ${getIlmPhaseDescription(option.label)}`}>
|
||||
<StyledOption>
|
||||
<StyledOptionLabel>{`${option.label}`}</StyledOptionLabel>
|
||||
{': '}
|
||||
<span>{getIlmPhaseDescription(option.label)}</span>
|
||||
</StyledOption>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const IlmPhaseFilterComponent: React.FC = () => {
|
||||
const { selectedIlmPhaseOptions, setSelectedIlmPhaseOptions } = useDataQualityContext();
|
||||
const labelInputId = useGeneratedHtmlId({ prefix: 'labelInput' });
|
||||
const ilmFormLabel = useMemo(
|
||||
() => <EuiFormLabel htmlFor={labelInputId}>{ILM_PHASE}</EuiFormLabel>,
|
||||
[labelInputId]
|
||||
);
|
||||
|
||||
const handleSetSelectedOptions = useCallback(
|
||||
(selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
setSelectedIlmPhaseOptions(selectedOptions);
|
||||
},
|
||||
[setSelectedIlmPhaseOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip display="block" content={INDEX_LIFECYCLE_MANAGEMENT_PHASES}>
|
||||
<StyledFormControlLayout fullWidth={true} prepend={ilmFormLabel}>
|
||||
<EuiComboBox
|
||||
id={labelInputId}
|
||||
data-test-subj="selectIlmPhases"
|
||||
fullWidth={true}
|
||||
placeholder={SELECT_ONE_OR_MORE_ILM_PHASES}
|
||||
renderOption={renderOption}
|
||||
selectedOptions={selectedIlmPhaseOptions}
|
||||
options={ilmPhaseOptionsStatic}
|
||||
onChange={handleSetSelectedOptions}
|
||||
/>
|
||||
</StyledFormControlLayout>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
IlmPhaseFilterComponent.displayName = 'IlmPhaseFilterComponent';
|
||||
|
||||
export const IlmPhaseFilter = React.memo(IlmPhaseFilterComponent);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormControlLayout } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const StyledOption = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const StyledOptionLabel = styled.span`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightBold};
|
||||
`;
|
||||
|
||||
export const StyledFormControlLayout = styled(EuiFormControlLayout)`
|
||||
height: 42px;
|
||||
|
||||
.euiFormControlLayout__childrenWrapper {
|
||||
overflow: visible;
|
||||
}
|
||||
`;
|
|
@ -13,9 +13,12 @@ import { EMPTY_STAT } from '../../helpers';
|
|||
import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../types';
|
||||
import { Props, DataQualitySummary } from '.';
|
||||
import { DataQualitySummary } from '.';
|
||||
import {
|
||||
getTotalDocsCount,
|
||||
getTotalIncompatible,
|
||||
|
@ -62,34 +65,33 @@ const totalIndices = getTotalIndices(patternRollups);
|
|||
const totalIndicesChecked = getTotalIndicesChecked(patternRollups);
|
||||
const totalSizeInBytes = getTotalSizeInBytes(patternRollups);
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: jest.fn(),
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
setLastChecked: jest.fn(),
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
onCheckCompleted: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DataQualitySummary', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<DataQualitySummary {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
lastChecked,
|
||||
patterns,
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
}}
|
||||
>
|
||||
<DataQualitySummary />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,108 +6,75 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getErrorSummaries } from '../../helpers';
|
||||
import { StatsRollup } from '../pattern/pattern_summary/stats_rollup';
|
||||
import { SummaryActions } from './summary_actions';
|
||||
import type { OnCheckCompleted, PatternRollup } from '../../types';
|
||||
import { IlmPhaseFilter } from './ilm_phase_filter';
|
||||
import { useDataQualityContext } from '../data_quality_context';
|
||||
import { useResultsRollupContext } from '../../contexts/results_rollup_context';
|
||||
|
||||
const MAX_SUMMARY_ACTIONS_CONTAINER_WIDTH = 400;
|
||||
const MIN_SUMMARY_ACTIONS_CONTAINER_WIDTH = 235;
|
||||
|
||||
const StyledFlexGroup = styled(EuiFlexGroup)`
|
||||
min-height: calc(174px - ${({ theme }) => theme.eui.euiSizeL} * 2);
|
||||
`;
|
||||
|
||||
const StyledFlexItem = styled(EuiFlexItem)`
|
||||
gap: ${({ theme }) => theme.eui.euiSizeL};
|
||||
`;
|
||||
|
||||
const SummaryActionsContainerFlexItem = styled(EuiFlexItem)`
|
||||
max-width: ${MAX_SUMMARY_ACTIONS_CONTAINER_WIDTH}px;
|
||||
min-width: ${MIN_SUMMARY_ACTIONS_CONTAINER_WIDTH}px;
|
||||
padding-right: ${({ theme }) => theme.eui.euiSizeXL};
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
ilmPhases: string[];
|
||||
lastChecked: string;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patternIndexNames: Record<string, string[]>;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
setLastChecked: (lastChecked: string) => void;
|
||||
totalDocsCount: number | undefined;
|
||||
totalIncompatible: number | undefined;
|
||||
totalIndices: number | undefined;
|
||||
totalIndicesChecked: number | undefined;
|
||||
totalSizeInBytes: number | undefined;
|
||||
onCheckCompleted: OnCheckCompleted;
|
||||
}
|
||||
const StyledIlmPhaseFilterContainer = styled.div`
|
||||
width: 100%;
|
||||
max-width: 432px;
|
||||
align-self: flex-end;
|
||||
`;
|
||||
|
||||
const DataQualitySummaryComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
onCheckCompleted,
|
||||
}) => {
|
||||
const errorSummary = useMemo(() => getErrorSummaries(patternRollups), [patternRollups]);
|
||||
const StyledRollupContainer = styled.div`
|
||||
margin-top: auto;
|
||||
`;
|
||||
|
||||
const DataQualitySummaryComponent: React.FC = () => {
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const { totalIndices, totalDocsCount, totalIndicesChecked, totalIncompatible, totalSizeInBytes } =
|
||||
useResultsRollupContext();
|
||||
|
||||
return (
|
||||
<EuiPanel data-test-subj="dataQualitySummary" hasShadow={true}>
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiPanel paddingSize="l" data-test-subj="dataQualitySummary" hasShadow={true}>
|
||||
<StyledFlexGroup
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
>
|
||||
<SummaryActionsContainerFlexItem grow={false}>
|
||||
<SummaryActions
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
errorSummary={errorSummary}
|
||||
ilmPhases={ilmPhases}
|
||||
lastChecked={lastChecked}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={patterns}
|
||||
patternRollups={patternRollups}
|
||||
setLastChecked={setLastChecked}
|
||||
sizeInBytes={totalSizeInBytes}
|
||||
totalDocsCount={totalDocsCount}
|
||||
totalIncompatible={totalIncompatible}
|
||||
totalIndices={totalIndices}
|
||||
totalIndicesChecked={totalIndicesChecked}
|
||||
/>
|
||||
<SummaryActions />
|
||||
</SummaryActionsContainerFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatsRollup
|
||||
docsCount={totalDocsCount}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
incompatible={totalIncompatible}
|
||||
indices={totalIndices}
|
||||
indicesChecked={totalIndicesChecked}
|
||||
sizeInBytes={totalSizeInBytes}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<StyledFlexItem grow={false}>
|
||||
{isILMAvailable && (
|
||||
<StyledIlmPhaseFilterContainer>
|
||||
<IlmPhaseFilter />
|
||||
</StyledIlmPhaseFilterContainer>
|
||||
)}
|
||||
<StyledRollupContainer>
|
||||
<StatsRollup
|
||||
docsCount={totalDocsCount}
|
||||
incompatible={totalIncompatible}
|
||||
indices={totalIndices}
|
||||
indicesChecked={totalIndicesChecked}
|
||||
sizeInBytes={totalSizeInBytes}
|
||||
/>
|
||||
</StyledRollupContainer>
|
||||
</StyledFlexItem>
|
||||
</StyledFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../../mock/test_providers/test_providers';
|
||||
import { Props, Actions } from '.';
|
||||
|
||||
const mockCopyToClipboard = jest.fn((value) => true);
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
...original,
|
||||
copyToClipboard: (value: string) => mockCopyToClipboard(value),
|
||||
};
|
||||
});
|
||||
|
||||
const ilmPhases = ['hot', 'warm', 'unmanaged'];
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: () => true,
|
||||
getMarkdownComments: () => [],
|
||||
ilmPhases,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Actions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when the action buttons are clicked', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Actions {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes openCreateCaseFlyout when the add to new case button is clicked', () => {
|
||||
const button = screen.getByTestId('addToNewCase');
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
expect(defaultProps.openCreateCaseFlyout).toBeCalled();
|
||||
});
|
||||
|
||||
test('it invokes addSuccessToast when the copy to clipboard button is clicked', () => {
|
||||
const button = screen.getByTestId('copyToClipboard');
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
expect(defaultProps.addSuccessToast).toBeCalledWith({
|
||||
title: 'Copied results to the clipboard',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('it disables the add to new case button when the user cannot create cases', () => {
|
||||
const canUserCreateAndReadCases = () => false;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Actions {...defaultProps} canUserCreateAndReadCases={canUserCreateAndReadCases} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('addToNewCase');
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
test('it disables the add to new case button when `ilmPhases` is empty', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Actions {...defaultProps} ilmPhases={[]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('addToNewCase');
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
test('it disables the copy to clipboard button when `ilmPhases` is empty', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Actions {...defaultProps} ilmPhases={[]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('copyToClipboard');
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { copyToClipboard, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
ADD_TO_NEW_CASE,
|
||||
COPIED_RESULTS_TOAST_TITLE,
|
||||
COPY_TO_CLIPBOARD,
|
||||
} from '../../../../translations';
|
||||
import { useAddToNewCase } from '../../../../use_add_to_new_case';
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
getMarkdownComments: () => string[];
|
||||
ilmPhases: string[];
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
getMarkdownComments,
|
||||
ilmPhases,
|
||||
openCreateCaseFlyout,
|
||||
}) => {
|
||||
const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
openCreateCaseFlyout,
|
||||
});
|
||||
|
||||
const onClickAddToCase = useCallback(
|
||||
() => onAddToNewCase([getMarkdownComments().join('\n')]),
|
||||
[getMarkdownComments, onAddToNewCase]
|
||||
);
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
const markdown = getMarkdownComments().join('\n');
|
||||
copyToClipboard(markdown);
|
||||
|
||||
addSuccessToast({
|
||||
title: COPIED_RESULTS_TOAST_TITLE,
|
||||
});
|
||||
}, [addSuccessToast, getMarkdownComments]);
|
||||
|
||||
const addToNewCaseContextMenuOnClick = useCallback(() => {
|
||||
onClickAddToCase();
|
||||
}, [onClickAddToCase]);
|
||||
|
||||
const disableAll = ilmPhases.length === 0;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="actions" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={ADD_TO_NEW_CASE}
|
||||
data-test-subj="addToNewCase"
|
||||
disabled={addToNewCaseDisabled || disableAll}
|
||||
flush="left"
|
||||
iconType="listAdd"
|
||||
onClick={addToNewCaseContextMenuOnClick}
|
||||
size="xs"
|
||||
>
|
||||
{ADD_TO_NEW_CASE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={COPY_TO_CLIPBOARD}
|
||||
data-test-subj="copyToClipboard"
|
||||
disabled={disableAll}
|
||||
iconType="copyClipboard"
|
||||
onClick={onCopy}
|
||||
size="xs"
|
||||
>
|
||||
{COPY_TO_CLIPBOARD}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsComponent.displayName = 'ActionsComponent';
|
||||
|
||||
export const Actions = React.memo(ActionsComponent);
|
|
@ -1,317 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EcsVersion } from '@elastic/ecs';
|
||||
|
||||
import { checkIndex, EMPTY_PARTITIONED_FIELD_METADATA } from './check_index';
|
||||
import { EMPTY_STAT } from '../../../../helpers';
|
||||
import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response';
|
||||
import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values';
|
||||
import { UnallowedValueRequestItem } from '../../../../types';
|
||||
import { EcsFlatTyped } from '../../../../constants';
|
||||
|
||||
let mockFetchMappings = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
new Promise((resolve) => {
|
||||
resolve(mockMappingsResponse); // happy path
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../../../../use_mappings/helpers', () => ({
|
||||
fetchMappings: ({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
mockFetchMappings({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockFetchUnallowedValues = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) => new Promise((resolve) => resolve(mockUnallowedValuesResponse))
|
||||
);
|
||||
|
||||
jest.mock('../../../../use_unallowed_values/helpers', () => {
|
||||
const original = jest.requireActual('../../../../use_unallowed_values/helpers');
|
||||
|
||||
return {
|
||||
...original,
|
||||
fetchUnallowedValues: ({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) =>
|
||||
mockFetchUnallowedValues({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('checkIndex', () => {
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
|
||||
|
||||
const defaultNumberFormat = '0,0.[000]';
|
||||
const formatNumber = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
|
||||
const indexName = 'auditbeat-custom-index-1';
|
||||
const pattern = 'auditbeat-*';
|
||||
const httpFetch = jest.fn();
|
||||
|
||||
describe('when `checkIndex` successfully completes the check', () => {
|
||||
const onCheckCompleted = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
await checkIndex({
|
||||
abortController: new AbortController(),
|
||||
batchId: 'batch-id',
|
||||
checkAllStartTime: Date.now(),
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck: false,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with a null `error`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].error).toBeNull();
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `indexName`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the non-default `partitionedFieldMetadata`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).not.toEqual(
|
||||
EMPTY_PARTITIONED_FIELD_METADATA
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected`pattern`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `version`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('happy path, when the signal is aborted', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it does NOT invoke onCheckCompleted', async () => {
|
||||
const onCheckCompleted = jest.fn();
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
await checkIndex({
|
||||
abortController,
|
||||
batchId: 'batch-id',
|
||||
checkAllStartTime: Date.now(),
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck: false,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
|
||||
expect(onCheckCompleted).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error occurs', () => {
|
||||
const onCheckCompleted = jest.fn();
|
||||
const error = 'simulated fetch mappings error';
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockFetchMappings = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) => new Promise((_, reject) => reject(new Error(error)))
|
||||
);
|
||||
|
||||
await checkIndex({
|
||||
abortController: new AbortController(),
|
||||
batchId: 'batch-id',
|
||||
checkAllStartTime: Date.now(),
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck: false,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `error`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].error).toEqual(error);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `indexName`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull();
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `pattern`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `version`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error occurs, but the error does not have a toString', () => {
|
||||
const onCheckCompleted = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockFetchMappings = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}) => new Promise((_, reject) => reject(undefined))
|
||||
);
|
||||
|
||||
await checkIndex({
|
||||
abortController: new AbortController(),
|
||||
batchId: 'batch-id',
|
||||
checkAllStartTime: Date.now(),
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck: false,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the fallback `error`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].error).toEqual(
|
||||
`An error occurred checking index ${indexName}`
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `indexName`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull();
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `pattern`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern);
|
||||
});
|
||||
|
||||
test('it invokes onCheckCompleted with the expected `version`', () => {
|
||||
expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error occurs, and the signal is aborted', () => {
|
||||
const onCheckCompleted = jest.fn();
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
const error = 'simulated fetch mappings error';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it does NOT invoke onCheckCompleted', async () => {
|
||||
mockFetchMappings = jest.fn(
|
||||
({
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) => new Promise((_, reject) => reject(new Error(error)))
|
||||
);
|
||||
|
||||
await checkIndex({
|
||||
abortController,
|
||||
batchId: 'batch-id',
|
||||
checkAllStartTime: Date.now(),
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck: false,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
|
||||
expect(onCheckCompleted).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,14 +11,13 @@ import { act, render, screen, waitFor } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response';
|
||||
import { TestProviders } from '../../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../../mock/test_providers/test_providers';
|
||||
import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values';
|
||||
import { CANCEL, CHECK_ALL } from '../../../../translations';
|
||||
import {
|
||||
OnCheckCompleted,
|
||||
PartitionedFieldMetadata,
|
||||
UnallowedValueRequestItem,
|
||||
} from '../../../../types';
|
||||
import { OnCheckCompleted, UnallowedValueRequestItem } from '../../../../types';
|
||||
import { CheckAll } from '.';
|
||||
import { EMPTY_STAT } from '../../../../helpers';
|
||||
|
||||
|
@ -30,64 +29,29 @@ const defaultNumberFormat = '0,0.[000]';
|
|||
const mockFormatNumber = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
|
||||
const mockFetchMappings = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
new Promise((resolve) => {
|
||||
resolve(mockMappingsResponse); // happy path
|
||||
})
|
||||
const mockFetchMappings = jest.fn(() =>
|
||||
Promise.resolve(
|
||||
mockMappingsResponse // happy path
|
||||
)
|
||||
);
|
||||
|
||||
jest.mock('../../../../use_mappings/helpers', () => ({
|
||||
fetchMappings: ({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
mockFetchMappings({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}),
|
||||
fetchMappings: (_: { abortController: AbortController; patternOrIndexName: string }) =>
|
||||
mockFetchMappings(),
|
||||
}));
|
||||
|
||||
const mockFetchUnallowedValues = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) => new Promise((resolve) => resolve(mockUnallowedValuesResponse))
|
||||
);
|
||||
const mockFetchUnallowedValues = jest.fn(() => Promise.resolve(mockUnallowedValuesResponse));
|
||||
|
||||
jest.mock('../../../../use_unallowed_values/helpers', () => {
|
||||
const original = jest.requireActual('../../../../use_unallowed_values/helpers');
|
||||
|
||||
return {
|
||||
...original,
|
||||
fetchUnallowedValues: ({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
fetchUnallowedValues: (_: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) =>
|
||||
mockFetchUnallowedValues({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}),
|
||||
}) => mockFetchUnallowedValues(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -120,20 +84,26 @@ describe('CheckAll', () => {
|
|||
|
||||
test('it renders the expected button text when a check is NOT running', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={jest.fn()}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkAll')).toHaveTextContent(CHECK_ALL);
|
||||
|
@ -141,20 +111,26 @@ describe('CheckAll', () => {
|
|||
|
||||
test('it renders a disabled button when ILM available and ilmPhases is an empty array', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={[]}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={jest.fn()}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
ilmPhases: [],
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeTruthy();
|
||||
|
@ -162,20 +138,27 @@ describe('CheckAll', () => {
|
|||
|
||||
test('it renders the expected button when ILM is NOT available', () => {
|
||||
render(
|
||||
<TestProviders isILMAvailable={false}>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={[]}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={jest.fn()}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases: [],
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeFalsy();
|
||||
|
@ -183,20 +166,27 @@ describe('CheckAll', () => {
|
|||
|
||||
test('it renders the expected button text when a check is running', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={jest.fn()}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('checkAll');
|
||||
|
@ -211,40 +201,35 @@ describe('CheckAll', () => {
|
|||
/** stores the result of invoking `CheckAll`'s `formatNumber` function */
|
||||
let formatNumberResult = '';
|
||||
|
||||
const onCheckCompleted: OnCheckCompleted = jest.fn(
|
||||
({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}: {
|
||||
error: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
indexName: string;
|
||||
partitionedFieldMetadata: PartitionedFieldMetadata | null;
|
||||
pattern: string;
|
||||
version: string;
|
||||
}) => {
|
||||
const value = 123456789; // numeric input to `CheckAll`'s `formatNumber` function
|
||||
const onCheckCompleted: OnCheckCompleted = jest.fn(({ formatBytes, formatNumber }) => {
|
||||
const value = 123456789; // numeric input to `CheckAll`'s `formatNumber` function
|
||||
|
||||
formatNumberResult = formatNumber(value);
|
||||
}
|
||||
);
|
||||
formatNumberResult = formatNumber(value);
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
onCheckCompleted,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('checkAll');
|
||||
|
@ -260,40 +245,34 @@ describe('CheckAll', () => {
|
|||
/** stores the result of invoking `CheckAll`'s `formatNumber` function */
|
||||
let formatNumberResult = '';
|
||||
|
||||
const onCheckCompleted: OnCheckCompleted = jest.fn(
|
||||
({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}: {
|
||||
error: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
indexName: string;
|
||||
partitionedFieldMetadata: PartitionedFieldMetadata | null;
|
||||
pattern: string;
|
||||
version: string;
|
||||
}) => {
|
||||
const value = undefined; // undefined input to `CheckAll`'s `formatNumber` function
|
||||
|
||||
formatNumberResult = formatNumber(value);
|
||||
}
|
||||
);
|
||||
const onCheckCompleted: OnCheckCompleted = jest.fn(({ formatBytes, formatNumber }) => {
|
||||
const value = undefined; // undefined input to `CheckAll`'s `formatNumber` function
|
||||
formatNumberResult = formatNumber(value);
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
onCheckCompleted,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('checkAll');
|
||||
|
@ -314,20 +293,27 @@ describe('CheckAll', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={jest.fn()}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={setCheckAllIndiciesChecked}
|
||||
setCheckAllTotalIndiciesToCheck={setCheckAllTotalIndiciesToCheck}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={setCheckAllIndiciesChecked}
|
||||
setCheckAllTotalIndiciesToCheck={setCheckAllTotalIndiciesToCheck}
|
||||
setIndexToCheck={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('checkAll');
|
||||
|
@ -358,20 +344,28 @@ describe('CheckAll', () => {
|
|||
jest.useFakeTimers();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<CheckAll
|
||||
formatBytes={mockFormatBytes}
|
||||
formatNumber={mockFormatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={[]}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={setIndexToCheck}
|
||||
/>
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
isILMAvailable: false,
|
||||
ilmPhases,
|
||||
formatNumber: mockFormatNumber,
|
||||
formatBytes: mockFormatBytes,
|
||||
patterns: [],
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
onCheckCompleted,
|
||||
}}
|
||||
>
|
||||
<CheckAll
|
||||
incrementCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllIndiciesChecked={jest.fn()}
|
||||
setCheckAllTotalIndiciesToCheck={jest.fn()}
|
||||
setIndexToCheck={setIndexToCheck}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('checkAll');
|
||||
|
|
|
@ -5,19 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EcsVersion } from '@elastic/ecs';
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { checkIndex } from './check_index';
|
||||
import { useResultsRollupContext } from '../../../../contexts/results_rollup_context';
|
||||
import { checkIndex } from '../../../../utils/check_index';
|
||||
import { useDataQualityContext } from '../../../data_quality_context';
|
||||
import { getAllIndicesToCheck } from './helpers';
|
||||
import * as i18n from '../../../../translations';
|
||||
import type { IndexToCheck, OnCheckCompleted } from '../../../../types';
|
||||
import { EcsFlatTyped } from '../../../../constants';
|
||||
import type { IndexToCheck } from '../../../../types';
|
||||
|
||||
const CheckAllButton = styled(EuiButton)`
|
||||
width: 112px;
|
||||
|
@ -35,13 +33,7 @@ async function wait(ms: number) {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
ilmPhases: string[];
|
||||
incrementCheckAllIndiciesChecked: () => void;
|
||||
onCheckCompleted: OnCheckCompleted;
|
||||
patternIndexNames: Record<string, string[]>;
|
||||
patterns: string[];
|
||||
setCheckAllIndiciesChecked: (checkAllIndiciesChecked: number) => void;
|
||||
setCheckAllTotalIndiciesToCheck: (checkAllTotalIndiciesToCheck: number) => void;
|
||||
setIndexToCheck: (indexToCheck: IndexToCheck | null) => void;
|
||||
|
@ -50,18 +42,14 @@ interface Props {
|
|||
const DELAY_AFTER_EVERY_CHECK_COMPLETES = 3000; // ms
|
||||
|
||||
const CheckAllComponent: React.FC<Props> = ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
incrementCheckAllIndiciesChecked,
|
||||
onCheckCompleted,
|
||||
patternIndexNames,
|
||||
patterns,
|
||||
setCheckAllIndiciesChecked,
|
||||
setCheckAllTotalIndiciesToCheck,
|
||||
setIndexToCheck,
|
||||
}) => {
|
||||
const { httpFetch, isILMAvailable } = useDataQualityContext();
|
||||
const { httpFetch, isILMAvailable, formatBytes, formatNumber, ilmPhases, patterns } =
|
||||
useDataQualityContext();
|
||||
const { onCheckCompleted, patternIndexNames } = useResultsRollupContext();
|
||||
const abortController = useRef(new AbortController());
|
||||
const [isRunning, setIsRunning] = useState<boolean>(false);
|
||||
|
||||
|
@ -98,16 +86,15 @@ const CheckAllComponent: React.FC<Props> = ({
|
|||
abortController: abortController.current,
|
||||
batchId,
|
||||
checkAllStartTime: startTime,
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isCheckAll: true,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isLastCheck:
|
||||
allIndicesToCheck.length > 0 ? checked === allIndicesToCheck.length - 1 : true,
|
||||
onCheckCompleted,
|
||||
pattern,
|
||||
version: EcsVersion,
|
||||
});
|
||||
|
||||
if (!abortController.current.signal.aborted) {
|
||||
|
|
|
@ -14,9 +14,12 @@ import { EMPTY_STAT } from '../../../helpers';
|
|||
import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../../types';
|
||||
import { Props, SummaryActions } from '.';
|
||||
import { SummaryActions } from '.';
|
||||
import {
|
||||
getTotalDocsCount,
|
||||
getTotalIncompatible,
|
||||
|
@ -64,6 +67,8 @@ const patternIndexNames: Record<string, string[]> = {
|
|||
],
|
||||
};
|
||||
|
||||
const addSuccessToast = jest.fn();
|
||||
|
||||
const lastChecked = '2023-03-28T23:27:28.159Z';
|
||||
|
||||
const totalDocsCount = getTotalDocsCount(patternRollups);
|
||||
|
@ -72,35 +77,34 @@ const totalIndices = getTotalIndices(patternRollups);
|
|||
const totalIndicesChecked = getTotalIndicesChecked(patternRollups);
|
||||
const totalSizeInBytes = getTotalSizeInBytes(patternRollups);
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: () => true,
|
||||
errorSummary: [],
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
lastChecked,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
onCheckCompleted: jest.fn(),
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
setLastChecked: jest.fn(),
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
sizeInBytes: totalSizeInBytes,
|
||||
};
|
||||
|
||||
describe('SummaryActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<SummaryActions {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhases,
|
||||
patterns,
|
||||
lastChecked,
|
||||
addSuccessToast,
|
||||
}}
|
||||
resultsRollupContextProps={{
|
||||
patternIndexNames,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
patternRollups,
|
||||
}}
|
||||
>
|
||||
<SummaryActions />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -121,7 +125,7 @@ describe('SummaryActions', () => {
|
|||
|
||||
userEvent.click(button);
|
||||
|
||||
expect(defaultProps.addSuccessToast).toBeCalledWith({
|
||||
expect(addSuccessToast).toBeCalledWith({
|
||||
title: 'Copied results to the clipboard',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { sortBy } from 'lodash/fp';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CheckAll } from './check_all';
|
||||
import { CheckStatus } from '../check_status';
|
||||
|
@ -22,16 +23,16 @@ import {
|
|||
getSummaryTableMarkdownRow,
|
||||
} from '../../index_properties/markdown/helpers';
|
||||
import { defaultSort, getSummaryTableItems } from '../../pattern/helpers';
|
||||
import { Actions } from './actions';
|
||||
import type {
|
||||
DataQualityCheckResult,
|
||||
ErrorSummary,
|
||||
IndexToCheck,
|
||||
OnCheckCompleted,
|
||||
PatternRollup,
|
||||
} from '../../../types';
|
||||
import { getSizeInBytes } from '../../../helpers';
|
||||
import type { DataQualityCheckResult, IndexToCheck, PatternRollup } from '../../../types';
|
||||
import { getErrorSummaries, getSizeInBytes } from '../../../helpers';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { useResultsRollupContext } from '../../../contexts/results_rollup_context';
|
||||
import { Actions } from '../../actions';
|
||||
|
||||
const StyledActionsContainerFlexItem = styled(EuiFlexItem)`
|
||||
margin-top: auto;
|
||||
padding-bottom: 3px;
|
||||
`;
|
||||
|
||||
export const getResultsSortedByDocsCount = (
|
||||
results: Record<string, DataQualityCheckResult> | undefined
|
||||
|
@ -135,54 +136,18 @@ export const getAllMarkdownComments = ({
|
|||
);
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
errorSummary: ErrorSummary[];
|
||||
ilmPhases: string[];
|
||||
lastChecked: string;
|
||||
onCheckCompleted: OnCheckCompleted;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
patternIndexNames: Record<string, string[]>;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
setLastChecked: (lastChecked: string) => void;
|
||||
totalDocsCount: number | undefined;
|
||||
totalIncompatible: number | undefined;
|
||||
totalIndices: number | undefined;
|
||||
totalIndicesChecked: number | undefined;
|
||||
sizeInBytes: number | undefined;
|
||||
}
|
||||
|
||||
const SummaryActionsComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
errorSummary,
|
||||
ilmPhases,
|
||||
lastChecked,
|
||||
onCheckCompleted,
|
||||
openCreateCaseFlyout,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
patterns,
|
||||
setLastChecked,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
sizeInBytes,
|
||||
}) => {
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const SummaryActionsComponent: React.FC = () => {
|
||||
const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext();
|
||||
const {
|
||||
patternRollups,
|
||||
totalIndices,
|
||||
totalDocsCount,
|
||||
totalIndicesChecked,
|
||||
totalIncompatible,
|
||||
patternIndexNames,
|
||||
totalSizeInBytes,
|
||||
} = useResultsRollupContext();
|
||||
const errorSummary = useMemo(() => getErrorSummaries(patternRollups), [patternRollups]);
|
||||
const [indexToCheck, setIndexToCheck] = useState<IndexToCheck | null>(null);
|
||||
const [checkAllIndiciesChecked, setCheckAllIndiciesChecked] = useState<number>(0);
|
||||
const [checkAllTotalIndiciesToCheck, setCheckAllTotalIndiciesToCheck] = useState<number>(0);
|
||||
|
@ -190,31 +155,32 @@ const SummaryActionsComponent: React.FC<Props> = ({
|
|||
setCheckAllIndiciesChecked((current) => current + 1);
|
||||
}, []);
|
||||
|
||||
const getMarkdownComments = useCallback(
|
||||
(): string[] => [
|
||||
getDataQualitySummaryMarkdownComment({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
sizeInBytes,
|
||||
}),
|
||||
...getAllMarkdownComments({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
}),
|
||||
getErrorsMarkdownTable({
|
||||
errorSummary,
|
||||
getMarkdownTableRows: getErrorsMarkdownTableRows,
|
||||
headerNames: [PATTERN, INDEX, ERROR],
|
||||
title: ERRORS,
|
||||
}),
|
||||
],
|
||||
const markdownComment = useMemo(
|
||||
() =>
|
||||
[
|
||||
getDataQualitySummaryMarkdownComment({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
sizeInBytes: totalSizeInBytes,
|
||||
}),
|
||||
...getAllMarkdownComments({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
}),
|
||||
getErrorsMarkdownTable({
|
||||
errorSummary,
|
||||
getMarkdownTableRows: getErrorsMarkdownTableRows,
|
||||
headerNames: [PATTERN, INDEX, ERROR],
|
||||
title: ERRORS,
|
||||
}),
|
||||
].join('\n'),
|
||||
[
|
||||
errorSummary,
|
||||
formatBytes,
|
||||
|
@ -222,11 +188,11 @@ const SummaryActionsComponent: React.FC<Props> = ({
|
|||
isILMAvailable,
|
||||
patternIndexNames,
|
||||
patternRollups,
|
||||
sizeInBytes,
|
||||
totalDocsCount,
|
||||
totalIncompatible,
|
||||
totalIndices,
|
||||
totalIndicesChecked,
|
||||
totalSizeInBytes,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -235,13 +201,7 @@ const SummaryActionsComponent: React.FC<Props> = ({
|
|||
<EuiFlexGroup data-test-subj="summaryActions" direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<CheckAll
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
ilmPhases={ilmPhases}
|
||||
incrementCheckAllIndiciesChecked={incrementCheckAllIndiciesChecked}
|
||||
onCheckCompleted={onCheckCompleted}
|
||||
patternIndexNames={patternIndexNames}
|
||||
patterns={patterns}
|
||||
setCheckAllIndiciesChecked={setCheckAllIndiciesChecked}
|
||||
setCheckAllTotalIndiciesToCheck={setCheckAllTotalIndiciesToCheck}
|
||||
setIndexToCheck={setIndexToCheck}
|
||||
|
@ -250,25 +210,20 @@ const SummaryActionsComponent: React.FC<Props> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<CheckStatus
|
||||
addSuccessToast={addSuccessToast}
|
||||
checkAllIndiciesChecked={checkAllIndiciesChecked}
|
||||
checkAllTotalIndiciesToCheck={checkAllTotalIndiciesToCheck}
|
||||
errorSummary={errorSummary}
|
||||
indexToCheck={indexToCheck}
|
||||
lastChecked={lastChecked}
|
||||
setLastChecked={setLastChecked}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledActionsContainerFlexItem grow={false}>
|
||||
<Actions
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
getMarkdownComments={getMarkdownComments}
|
||||
ilmPhases={ilmPhases}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
showAddToNewCaseAction={true}
|
||||
showCopyToClipboardAction={true}
|
||||
markdownComment={markdownComment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</StyledActionsContainerFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { ErrorEmptyPrompt } from '.';
|
||||
|
||||
describe('ErrorEmptyPrompt', () => {
|
||||
|
@ -16,9 +16,9 @@ describe('ErrorEmptyPrompt', () => {
|
|||
const title = 'This is the title of this work';
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<ErrorEmptyPrompt title={title} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('errorEmptyPrompt').textContent?.includes(title)).toBe(true);
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { IlmPhaseCounts } from '.';
|
||||
import { getIlmExplainPhaseCounts } from '../pattern/helpers';
|
||||
|
||||
|
@ -74,9 +74,9 @@ const pattern = 'packetbeat-*';
|
|||
describe('IlmPhaseCounts', () => {
|
||||
test('it renders the expected counts', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<IlmPhaseCounts ilmExplainPhaseCounts={ilmExplainPhaseCounts} pattern={pattern} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ilmPhaseCounts')).toHaveTextContent(
|
||||
|
|
|
@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { EmptyPromptBody } from './empty_prompt_body';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
|
||||
describe('EmptyPromptBody', () => {
|
||||
const content = 'foo bar baz @baz';
|
||||
|
||||
test('it renders the expected content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<EmptyPromptBody body={content} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('emptyPromptBody')).toHaveTextContent(content);
|
||||
|
|
|
@ -9,16 +9,16 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { EmptyPromptTitle } from './empty_prompt_title';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
|
||||
describe('EmptyPromptTitle', () => {
|
||||
const title = 'What is a great title?';
|
||||
|
||||
test('it renders the expected content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<EmptyPromptTitle title={title} />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('emptyPromptTitle')).toHaveTextContent(title);
|
||||
|
|
|
@ -25,7 +25,6 @@ export const ECS_COMPLIANT_TAB_ID = 'ecsCompliantTab';
|
|||
export const CUSTOM_TAB_ID = 'customTab';
|
||||
export const INCOMPATIBLE_TAB_ID = 'incompatibleTab';
|
||||
export const SAME_FAMILY_TAB_ID = 'sameFamilyTab';
|
||||
export const SUMMARY_TAB_ID = 'summaryTab';
|
||||
|
||||
export const EMPTY_METADATA: PartitionedFieldMetadata = {
|
||||
all: [],
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { HttpHandler } from '@kbn/core-http-browser';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { EMPTY_STAT } from '../../helpers';
|
||||
import { mockMappingsResponse } from '../../mock/mappings_response/mock_mappings_response';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_unallowed_values';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import { LOADING_MAPPINGS, LOADING_UNALLOWED_VALUES } from './translations';
|
||||
import { UnallowedValueRequestItem } from '../../types';
|
||||
import { IndexProperties, Props } from '.';
|
||||
import { getCheckState } from '../../stub/get_check_state';
|
||||
|
||||
const indexName = 'auditbeat-custom-index-1';
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
|
||||
|
@ -31,101 +31,32 @@ const formatNumber = (value: number | undefined) =>
|
|||
const pattern = 'auditbeat-*';
|
||||
const patternRollup = auditbeatWithAllResults;
|
||||
|
||||
const mockFetchMappings = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
httpFetch,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
httpFetch: HttpHandler;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
new Promise((resolve) => {
|
||||
resolve(mockMappingsResponse); // happy path
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../../use_mappings/helpers', () => ({
|
||||
fetchMappings: ({
|
||||
abortController,
|
||||
httpFetch,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
httpFetch: HttpHandler;
|
||||
patternOrIndexName: string;
|
||||
}) =>
|
||||
mockFetchMappings({
|
||||
abortController,
|
||||
httpFetch,
|
||||
patternOrIndexName,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockFetchUnallowedValues = jest.fn(
|
||||
({
|
||||
abortController,
|
||||
httpFetch,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
httpFetch: HttpHandler;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) => new Promise((resolve) => resolve(mockUnallowedValuesResponse))
|
||||
);
|
||||
|
||||
jest.mock('../../use_unallowed_values/helpers', () => {
|
||||
const original = jest.requireActual('../../use_unallowed_values/helpers');
|
||||
|
||||
return {
|
||||
...original,
|
||||
fetchUnallowedValues: ({
|
||||
abortController,
|
||||
httpFetch,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
httpFetch: HttpHandler;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) =>
|
||||
mockFetchUnallowedValues({
|
||||
abortController,
|
||||
httpFetch,
|
||||
indexName,
|
||||
requestItems,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const defaultProps: Props = {
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: jest.fn(),
|
||||
docsCount: auditbeatWithAllResults.docsCount ?? 0,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick: jest.fn(),
|
||||
indexId: '1xxx',
|
||||
ilmPhase: 'hot',
|
||||
indexName: 'auditbeat-custom-index-1',
|
||||
isAssistantEnabled: true,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
pattern,
|
||||
patternRollup,
|
||||
baseTheme: DARK_THEME,
|
||||
updatePatternRollup: jest.fn(),
|
||||
};
|
||||
|
||||
describe('IndexProperties', () => {
|
||||
test('it renders the tab content', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: {
|
||||
...getCheckState(indexName),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -144,21 +75,24 @@ describe('IndexProperties', () => {
|
|||
});
|
||||
|
||||
test('it displays the expected empty prompt content', async () => {
|
||||
mockFetchMappings.mockImplementation(
|
||||
({
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) => new Promise((_, reject) => reject(new Error(error)))
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: {
|
||||
...getCheckState(indexName, {
|
||||
mappingsError: new Error(error),
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -182,23 +116,24 @@ describe('IndexProperties', () => {
|
|||
});
|
||||
|
||||
test('it displays the expected empty prompt content', async () => {
|
||||
mockFetchUnallowedValues.mockImplementation(
|
||||
({
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) => new Promise((_, reject) => reject(new Error(error)))
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: {
|
||||
...getCheckState(indexName, {
|
||||
unallowedValuesError: new Error(error),
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -217,20 +152,24 @@ describe('IndexProperties', () => {
|
|||
});
|
||||
|
||||
test('it displays the expected loading prompt content', async () => {
|
||||
mockFetchMappings.mockImplementation(
|
||||
({
|
||||
abortController,
|
||||
patternOrIndexName,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
patternOrIndexName: string;
|
||||
}) => new Promise(() => {}) // <-- will never resolve or reject
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: {
|
||||
...getCheckState(indexName, {
|
||||
isLoadingMappings: true,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -247,22 +186,24 @@ describe('IndexProperties', () => {
|
|||
});
|
||||
|
||||
test('it displays the expected loading prompt content', async () => {
|
||||
mockFetchUnallowedValues.mockImplementation(
|
||||
({
|
||||
abortController,
|
||||
indexName,
|
||||
requestItems,
|
||||
}: {
|
||||
abortController: AbortController;
|
||||
indexName: string;
|
||||
requestItems: UnallowedValueRequestItem[];
|
||||
}) => new Promise(() => {}) // <-- will never resolve or reject
|
||||
);
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: {
|
||||
...getCheckState(indexName, {
|
||||
isLoadingUnallowedValues: true,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IndexProperties {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -5,350 +5,83 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EcsVersion } from '@elastic/ecs';
|
||||
import type {
|
||||
FlameElementEvent,
|
||||
HeatmapElementEvent,
|
||||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import React from 'react';
|
||||
|
||||
import { getUnallowedValueRequestItems } from '../allowed_values/helpers';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ErrorEmptyPrompt } from '../error_empty_prompt';
|
||||
import {
|
||||
EMPTY_METADATA,
|
||||
getMappingsProperties,
|
||||
getSortedPartitionedFieldMetadata,
|
||||
hasAllDataFetchingCompleted,
|
||||
INCOMPATIBLE_TAB_ID,
|
||||
} from './helpers';
|
||||
import { LoadingEmptyPrompt } from '../loading_empty_prompt';
|
||||
import { getIndexPropertiesContainerId } from '../pattern/helpers';
|
||||
import { getTabs } from '../tabs/helpers';
|
||||
import {
|
||||
getAllIncompatibleMarkdownComments,
|
||||
getIncompatibleValuesFields,
|
||||
getIncompatibleMappingsFields,
|
||||
getSameFamilyFields,
|
||||
} from '../tabs/incompatible_tab/helpers';
|
||||
import * as i18n from './translations';
|
||||
import type { IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../../types';
|
||||
import { useAddToNewCase } from '../../use_add_to_new_case';
|
||||
import { useMappings } from '../../use_mappings';
|
||||
import { useUnallowedValues } from '../../use_unallowed_values';
|
||||
import type { IlmPhase, PatternRollup } from '../../types';
|
||||
import { useIndicesCheckContext } from '../../contexts/indices_check_context';
|
||||
import { IndexCheckFields } from './index_check_fields';
|
||||
import { IndexStatsPanel } from './index_stats_panel';
|
||||
import { useDataQualityContext } from '../data_quality_context';
|
||||
import { formatStorageResult, postStorageResult, getSizeInBytes } from '../../helpers';
|
||||
import { EcsFlatTyped } from '../../constants';
|
||||
|
||||
const EMPTY_MARKDOWN_COMMENTS: string[] = [];
|
||||
|
||||
export interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
docsCount: number;
|
||||
getGroupByFieldsOnClick: (
|
||||
elements: Array<
|
||||
| FlameElementEvent
|
||||
| HeatmapElementEvent
|
||||
| MetricElementEvent
|
||||
| PartitionElementEvent
|
||||
| WordCloudElementEvent
|
||||
| XYChartElementEvent
|
||||
>
|
||||
) => {
|
||||
groupByField0: string;
|
||||
groupByField1: string;
|
||||
};
|
||||
ilmPhase: IlmPhase | undefined;
|
||||
indexId: string | null | undefined;
|
||||
indexName: string;
|
||||
isAssistantEnabled: boolean;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
pattern: string;
|
||||
patternRollup: PatternRollup | undefined;
|
||||
theme?: PartialTheme;
|
||||
baseTheme: Theme;
|
||||
updatePatternRollup: (patternRollup: PatternRollup) => void;
|
||||
sizeInBytes?: number;
|
||||
}
|
||||
|
||||
const IndexPropertiesComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
baseTheme,
|
||||
canUserCreateAndReadCases,
|
||||
docsCount,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhase,
|
||||
indexId,
|
||||
indexName,
|
||||
isAssistantEnabled,
|
||||
openCreateCaseFlyout,
|
||||
pattern,
|
||||
patternRollup,
|
||||
theme,
|
||||
updatePatternRollup,
|
||||
sizeInBytes,
|
||||
}) => {
|
||||
const { error: mappingsError, indexes, loading: loadingMappings } = useMappings(indexName);
|
||||
const { telemetryEvents, isILMAvailable, httpFetch, toasts } = useDataQualityContext();
|
||||
|
||||
const requestItems = useMemo(
|
||||
() =>
|
||||
getUnallowedValueRequestItems({
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
indexName,
|
||||
}),
|
||||
[indexName]
|
||||
);
|
||||
|
||||
const {
|
||||
error: unallowedValuesError,
|
||||
loading: loadingUnallowedValues,
|
||||
unallowedValues,
|
||||
requestTime,
|
||||
} = useUnallowedValues({ indexName, requestItems });
|
||||
|
||||
const mappingsProperties = useMemo(
|
||||
() =>
|
||||
getMappingsProperties({
|
||||
indexes,
|
||||
indexName,
|
||||
}),
|
||||
[indexName, indexes]
|
||||
);
|
||||
|
||||
const partitionedFieldMetadata: PartitionedFieldMetadata | null = useMemo(
|
||||
() =>
|
||||
getSortedPartitionedFieldMetadata({
|
||||
ecsMetadata: EcsFlatTyped,
|
||||
loadingMappings,
|
||||
mappingsProperties,
|
||||
unallowedValues,
|
||||
}),
|
||||
[loadingMappings, mappingsProperties, unallowedValues]
|
||||
);
|
||||
|
||||
const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
indexName,
|
||||
openCreateCaseFlyout,
|
||||
});
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<string>(INCOMPATIBLE_TAB_ID);
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
getTabs({
|
||||
addSuccessToast,
|
||||
addToNewCaseDisabled,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
docsCount,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhase,
|
||||
isAssistantEnabled,
|
||||
indexName,
|
||||
onAddToNewCase,
|
||||
partitionedFieldMetadata: partitionedFieldMetadata ?? EMPTY_METADATA,
|
||||
pattern,
|
||||
patternDocsCount: patternRollup?.docsCount ?? 0,
|
||||
setSelectedTabId,
|
||||
stats: patternRollup?.stats ?? null,
|
||||
theme,
|
||||
baseTheme,
|
||||
}),
|
||||
[
|
||||
addSuccessToast,
|
||||
addToNewCaseDisabled,
|
||||
docsCount,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmPhase,
|
||||
indexName,
|
||||
isAssistantEnabled,
|
||||
onAddToNewCase,
|
||||
partitionedFieldMetadata,
|
||||
pattern,
|
||||
patternRollup?.docsCount,
|
||||
patternRollup?.stats,
|
||||
theme,
|
||||
baseTheme,
|
||||
]
|
||||
);
|
||||
|
||||
const onSelectedTabChanged = useCallback((id: string) => {
|
||||
setSelectedTabId(id);
|
||||
}, []);
|
||||
|
||||
const selectedTabContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{tabs.find((obj) => obj.id === selectedTabId)?.content}
|
||||
</>
|
||||
),
|
||||
[selectedTabId, tabs]
|
||||
);
|
||||
|
||||
const renderTabs = useCallback(
|
||||
() =>
|
||||
tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
append={tab.append}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
key={index}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
)),
|
||||
[onSelectedTabChanged, selectedTabId, tabs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAllDataFetchingCompleted({ loadingMappings, loadingUnallowedValues })) {
|
||||
const error: string | null = mappingsError ?? unallowedValuesError;
|
||||
const indexIncompatible: number | undefined =
|
||||
error == null && partitionedFieldMetadata != null
|
||||
? partitionedFieldMetadata.incompatible.length
|
||||
: undefined;
|
||||
|
||||
const indexSameFamily: number | undefined =
|
||||
error == null && partitionedFieldMetadata != null
|
||||
? partitionedFieldMetadata.sameFamily.length
|
||||
: undefined;
|
||||
|
||||
if (patternRollup != null) {
|
||||
const markdownComments =
|
||||
partitionedFieldMetadata != null
|
||||
? getAllIncompatibleMarkdownComments({
|
||||
docsCount,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmPhase,
|
||||
indexName,
|
||||
isILMAvailable,
|
||||
partitionedFieldMetadata,
|
||||
patternDocsCount: patternRollup.docsCount ?? 0,
|
||||
sizeInBytes: patternRollup.sizeInBytes,
|
||||
})
|
||||
: EMPTY_MARKDOWN_COMMENTS;
|
||||
|
||||
const checkedAt = partitionedFieldMetadata ? Date.now() : undefined;
|
||||
|
||||
const updatedRollup = {
|
||||
...patternRollup,
|
||||
results: {
|
||||
...patternRollup.results,
|
||||
[indexName]: {
|
||||
docsCount,
|
||||
error,
|
||||
ilmPhase,
|
||||
incompatible: indexIncompatible,
|
||||
indexName,
|
||||
markdownComments,
|
||||
pattern,
|
||||
sameFamily: indexSameFamily,
|
||||
checkedAt,
|
||||
},
|
||||
},
|
||||
};
|
||||
updatePatternRollup(updatedRollup);
|
||||
|
||||
if (indexName && requestTime != null && requestTime > 0 && partitionedFieldMetadata) {
|
||||
const report = {
|
||||
batchId: uuidv4(),
|
||||
ecsVersion: EcsVersion,
|
||||
errorCount: error ? 1 : 0,
|
||||
ilmPhase,
|
||||
indexId,
|
||||
indexName,
|
||||
isCheckAll: false,
|
||||
numberOfDocuments: docsCount,
|
||||
numberOfFields: partitionedFieldMetadata.all.length,
|
||||
numberOfIncompatibleFields: indexIncompatible,
|
||||
numberOfEcsFields: partitionedFieldMetadata.ecsCompliant.length,
|
||||
numberOfCustomFields: partitionedFieldMetadata.custom.length,
|
||||
numberOfIndices: 1,
|
||||
numberOfIndicesChecked: 1,
|
||||
numberOfSameFamily: indexSameFamily,
|
||||
sizeInBytes: getSizeInBytes({ stats: patternRollup.stats, indexName }),
|
||||
timeConsumedMs: requestTime,
|
||||
sameFamilyFields: getSameFamilyFields(partitionedFieldMetadata.sameFamily),
|
||||
unallowedMappingFields: getIncompatibleMappingsFields(
|
||||
partitionedFieldMetadata.incompatible
|
||||
),
|
||||
unallowedValueFields: getIncompatibleValuesFields(
|
||||
partitionedFieldMetadata.incompatible
|
||||
),
|
||||
};
|
||||
telemetryEvents.reportDataQualityIndexChecked?.(report);
|
||||
|
||||
const result = updatedRollup.results[indexName];
|
||||
if (result) {
|
||||
const storageResult = formatStorageResult({ result, report, partitionedFieldMetadata });
|
||||
postStorageResult({ storageResult, httpFetch, toasts });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
docsCount,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
httpFetch,
|
||||
ilmPhase,
|
||||
indexId,
|
||||
indexName,
|
||||
isILMAvailable,
|
||||
loadingMappings,
|
||||
loadingUnallowedValues,
|
||||
mappingsError,
|
||||
partitionedFieldMetadata,
|
||||
pattern,
|
||||
patternRollup,
|
||||
requestTime,
|
||||
telemetryEvents,
|
||||
toasts,
|
||||
unallowedValuesError,
|
||||
updatePatternRollup,
|
||||
]);
|
||||
const { checkState } = useIndicesCheckContext();
|
||||
const { formatBytes, formatNumber } = useDataQualityContext();
|
||||
const indexCheckState = checkState[indexName];
|
||||
const isChecking = indexCheckState?.isChecking ?? false;
|
||||
const isLoadingMappings = indexCheckState?.isLoadingMappings ?? false;
|
||||
const isLoadingUnallowedValues = indexCheckState?.isLoadingUnallowedValues ?? false;
|
||||
const genericCheckError = indexCheckState?.genericError ?? null;
|
||||
const mappingsError = indexCheckState?.mappingsError ?? null;
|
||||
const unallowedValuesError = indexCheckState?.unallowedValuesError ?? null;
|
||||
const isCheckComplete = indexCheckState?.isCheckComplete ?? false;
|
||||
|
||||
if (mappingsError != null) {
|
||||
return <ErrorEmptyPrompt title={i18n.ERROR_LOADING_MAPPINGS_TITLE} />;
|
||||
} else if (unallowedValuesError != null) {
|
||||
return <ErrorEmptyPrompt title={i18n.ERROR_LOADING_UNALLOWED_VALUES_TITLE} />;
|
||||
} else if (genericCheckError != null) {
|
||||
return <ErrorEmptyPrompt title={i18n.ERROR_GENERIC_CHECK_TITLE} />;
|
||||
}
|
||||
|
||||
if (loadingMappings) {
|
||||
if (isLoadingMappings) {
|
||||
return <LoadingEmptyPrompt loading={i18n.LOADING_MAPPINGS} />;
|
||||
} else if (loadingUnallowedValues) {
|
||||
} else if (isLoadingUnallowedValues) {
|
||||
return <LoadingEmptyPrompt loading={i18n.LOADING_UNALLOWED_VALUES} />;
|
||||
} else if (isChecking) {
|
||||
return <LoadingEmptyPrompt loading={i18n.CHECKING_INDEX} />;
|
||||
}
|
||||
|
||||
return indexes != null ? (
|
||||
return isCheckComplete ? (
|
||||
<div data-index-properties-container={getIndexPropertiesContainerId({ indexName, pattern })}>
|
||||
<EuiTabs>{renderTabs()}</EuiTabs>
|
||||
{selectedTabContent}
|
||||
{ilmPhase && (
|
||||
<IndexStatsPanel
|
||||
docsCount={formatNumber(docsCount)}
|
||||
sizeInBytes={formatBytes(sizeInBytes ?? 0)}
|
||||
ilmPhase={ilmPhase}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<IndexCheckFields
|
||||
ilmPhase={ilmPhase}
|
||||
docsCount={docsCount}
|
||||
patternRollup={patternRollup}
|
||||
indexName={indexName}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
IndexPropertiesComponent.displayName = 'IndexPropertiesComponent';
|
||||
|
||||
export const IndexProperties = React.memo(IndexPropertiesComponent);
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
|
||||
import { IndexCheckFields } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('IndexCheckFields', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IndexCheckFields
|
||||
indexName="indexName"
|
||||
docsCount={123}
|
||||
ilmPhase="hot"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
it('should render the index check fields', () => {
|
||||
expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render incompatible tab content by default', () => {
|
||||
expect(screen.getByTestId('incompatibleTab')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('incompatibleTabContent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['sameFamilyTab', 'sameFamilyTabContent'],
|
||||
['customTab', 'customTabContent'],
|
||||
['ecsCompliantTab', 'ecsCompliantTabContent'],
|
||||
['allTab', 'allTabContent'],
|
||||
])('when clicking on %s tab', (tab, tabContent) => {
|
||||
it(`should render ${tabContent} content`, () => {
|
||||
userEvent.click(screen.getByTestId(tab));
|
||||
|
||||
expect(screen.getByTestId(tabContent)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EuiButtonGroup, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { useIndicesCheckContext } from '../../../contexts/indices_check_context';
|
||||
import { EMPTY_METADATA, INCOMPATIBLE_TAB_ID } from '../helpers';
|
||||
import { IlmPhase, PatternRollup } from '../../../types';
|
||||
import { getTabs } from '../../tabs/helpers';
|
||||
|
||||
const StyledTabFlexGroup = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTabFlexItem = styled.div`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledButtonGroup = styled(EuiButtonGroup)`
|
||||
button[data-test-subj='incompatibleTab'] {
|
||||
flex-grow: 1.2;
|
||||
}
|
||||
button[data-test-subj='ecsCompliantTab'] {
|
||||
flex-grow: 1.4;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
docsCount: number;
|
||||
ilmPhase: IlmPhase | undefined;
|
||||
indexName: string;
|
||||
patternRollup: PatternRollup | undefined;
|
||||
}
|
||||
|
||||
const IndexCheckFieldsComponent: React.FC<Props> = ({
|
||||
indexName,
|
||||
ilmPhase,
|
||||
patternRollup,
|
||||
docsCount,
|
||||
}) => {
|
||||
const { formatBytes, formatNumber } = useDataQualityContext();
|
||||
const { checkState } = useIndicesCheckContext();
|
||||
const partitionedFieldMetadata = checkState[indexName]?.partitionedFieldMetadata ?? null;
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<string>(INCOMPATIBLE_TAB_ID);
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
getTabs({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
docsCount,
|
||||
ilmPhase,
|
||||
indexName,
|
||||
partitionedFieldMetadata: partitionedFieldMetadata ?? EMPTY_METADATA,
|
||||
patternDocsCount: patternRollup?.docsCount ?? 0,
|
||||
stats: patternRollup?.stats ?? null,
|
||||
}),
|
||||
[
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
docsCount,
|
||||
ilmPhase,
|
||||
indexName,
|
||||
partitionedFieldMetadata,
|
||||
patternRollup?.docsCount,
|
||||
patternRollup?.stats,
|
||||
]
|
||||
);
|
||||
|
||||
const tabSelections = tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
label: (
|
||||
<StyledTabFlexGroup
|
||||
responsive={false}
|
||||
justifyContent="center"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
title={tab.name}
|
||||
>
|
||||
<StyledTabFlexItem>{tab.name}</StyledTabFlexItem>
|
||||
{tab.append}
|
||||
</StyledTabFlexGroup>
|
||||
),
|
||||
textProps: false as false,
|
||||
}));
|
||||
|
||||
const handleSelectedTabId = (optionId: string) => {
|
||||
setSelectedTabId(optionId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test-subj="indexCheckFields">
|
||||
<StyledButtonGroup
|
||||
legend="Index check field tab toggle"
|
||||
options={tabSelections}
|
||||
idSelected={selectedTabId}
|
||||
onChange={handleSelectedTabId}
|
||||
buttonSize="compressed"
|
||||
color="primary"
|
||||
isFullWidth
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{tabs.find((tab) => tab.id === selectedTabId)?.content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
IndexCheckFieldsComponent.displayName = 'IndexFieldsComponent';
|
||||
|
||||
export const IndexCheckFields = React.memo(IndexCheckFieldsComponent);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
|
||||
import { IndexStatsPanel } from '.';
|
||||
import { TestExternalProviders } from '../../../mock/test_providers/test_providers';
|
||||
|
||||
describe('IndexStatsPanel', () => {
|
||||
it('renders stats panel', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<IndexStatsPanel docsCount="123" ilmPhase="hot" sizeInBytes="789" />
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId('indexStatsPanel');
|
||||
|
||||
expect(container).toHaveTextContent('Docs123');
|
||||
expect(container).toHaveTextContent('ILM phasehot');
|
||||
expect(container).toHaveTextContent('Size789');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DOCS } from '../translations';
|
||||
import { ILM_PHASE } from '../../../translations';
|
||||
import { SIZE } from '../../summary_table/translations';
|
||||
import { Stat } from '../../pattern/pattern_summary/stats_rollup/stat';
|
||||
import { getIlmPhaseDescription } from '../../../helpers';
|
||||
|
||||
const StyledFlexItem = styled(EuiFlexItem)`
|
||||
border-right: 1px solid ${({ theme }) => theme.eui.euiBorderColor};
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
docsCount: string;
|
||||
ilmPhase: string;
|
||||
sizeInBytes: string;
|
||||
}
|
||||
|
||||
export const IndexStatsPanelComponent: React.FC<Props> = ({ docsCount, ilmPhase, sizeInBytes }) => (
|
||||
<EuiPanel data-test-subj="indexStatsPanel" paddingSize="s" hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<StyledFlexItem>
|
||||
<strong>{DOCS}</strong>
|
||||
<EuiSpacer />
|
||||
{docsCount}
|
||||
</StyledFlexItem>
|
||||
<StyledFlexItem>
|
||||
<div>
|
||||
<strong>{ILM_PHASE}</strong>
|
||||
<EuiSpacer />
|
||||
<Stat
|
||||
badgeText={ilmPhase}
|
||||
tooltipText={getIlmPhaseDescription(ilmPhase)}
|
||||
badgeProps={{ 'data-test-subj': 'ilmPhase' }}
|
||||
/>
|
||||
</div>
|
||||
</StyledFlexItem>
|
||||
<StyledFlexItem>
|
||||
<strong>{SIZE}</strong>
|
||||
<EuiSpacer />
|
||||
{sizeInBytes}
|
||||
</StyledFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
IndexStatsPanelComponent.displayName = 'IndexStatsPanelComponent';
|
||||
|
||||
export const IndexStatsPanel = React.memo(IndexStatsPanelComponent);
|
|
@ -217,6 +217,13 @@ export const ERROR_LOADING_UNALLOWED_VALUES_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ERROR_GENERIC_CHECK_TITLE = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorGenericCheckTitle',
|
||||
{
|
||||
defaultMessage: 'An error occurred during the check',
|
||||
}
|
||||
);
|
||||
|
||||
export const ECS_COMPLIANT_FIELDS_TABLE_TITLE = (indexName: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.customTab.ecsComplaintFieldsTableTitle',
|
||||
|
@ -240,10 +247,10 @@ export const LOADING_UNALLOWED_VALUES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SUMMARY = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryTab',
|
||||
export const CHECKING_INDEX = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.checkingIndexPrompt',
|
||||
{
|
||||
defaultMessage: 'Summary',
|
||||
defaultMessage: 'Checking index',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../summary_table/translations';
|
||||
import { getIndexResultBadgeColor, getIndexResultToolTip } from './helpers';
|
||||
|
||||
describe('getIndexResultBadgeColor', () => {
|
||||
test('it returns `ghost` when `incompatible` is undefined', () => {
|
||||
expect(getIndexResultBadgeColor(undefined)).toEqual('ghost');
|
||||
});
|
||||
|
||||
test('it returns `success` when `incompatible` is zero', () => {
|
||||
expect(getIndexResultBadgeColor(0)).toEqual('#6dcbb1');
|
||||
});
|
||||
|
||||
test('it returns `danger` when `incompatible` is NOT zero', () => {
|
||||
expect(getIndexResultBadgeColor(1)).toEqual('danger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIndexResultToolTip', () => {
|
||||
test('it returns "this index has not been checked" when `incompatible` is undefined', () => {
|
||||
expect(getIndexResultToolTip(undefined)).toEqual(THIS_INDEX_HAS_NOT_BEEN_CHECKED);
|
||||
});
|
||||
|
||||
test('it returns Passed when `incompatible` is zero', () => {
|
||||
expect(getIndexResultToolTip(0)).toEqual(PASSED);
|
||||
});
|
||||
|
||||
test('it returns Failed when `incompatible` is NOT zero', () => {
|
||||
expect(getIndexResultToolTip(1)).toEqual(FAILED);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from '../summary_table/translations';
|
||||
|
||||
export const getIndexResultBadgeColor = (incompatible: number | undefined): string => {
|
||||
if (incompatible == null) {
|
||||
return 'ghost';
|
||||
} else if (incompatible === 0) {
|
||||
return '#6dcbb1';
|
||||
} else {
|
||||
return 'danger';
|
||||
}
|
||||
};
|
||||
|
||||
export const getIndexResultToolTip = (incompatible: number | undefined): string => {
|
||||
if (incompatible == null) {
|
||||
return THIS_INDEX_HAS_NOT_BEEN_CHECKED;
|
||||
} else if (incompatible === 0) {
|
||||
return PASSED;
|
||||
} else {
|
||||
return FAILED;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, waitFor, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { IndexResultBadge } from '.';
|
||||
|
||||
describe('IndexResultBadge', () => {
|
||||
it('should render the index result badge', () => {
|
||||
render(<IndexResultBadge incompatible={0} />);
|
||||
|
||||
expect(screen.getByTestId('indexResultBadge')).toHaveTextContent('Pass');
|
||||
});
|
||||
|
||||
describe('when incompatible is > 0', () => {
|
||||
it('should render the index result badge with `Fail` content', () => {
|
||||
render(<IndexResultBadge incompatible={1} />);
|
||||
|
||||
expect(screen.getByTestId('indexResultBadge')).toHaveTextContent('Fail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tooltipText is given', () => {
|
||||
it('should render the index result badge with the given tooltip text', async () => {
|
||||
render(<IndexResultBadge incompatible={0} tooltipText="Tooltip text" />);
|
||||
|
||||
userEvent.hover(screen.getByTestId('indexResultBadge'));
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip text'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getIndexResultBadgeColor, getIndexResultToolTip } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const StyledBadge = styled(EuiBadge)`
|
||||
width: 44px;
|
||||
text-align: center;
|
||||
padding-inline: 0;
|
||||
|
||||
.euiBadge__content {
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export type Props = React.ComponentProps<typeof EuiBadge> & {
|
||||
incompatible: number;
|
||||
tooltipText?: string;
|
||||
};
|
||||
|
||||
export const IndexResultBadgeComponent: React.FC<Props> = ({
|
||||
incompatible,
|
||||
tooltipText,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<EuiToolTip content={tooltipText ?? getIndexResultToolTip(incompatible)}>
|
||||
<StyledBadge
|
||||
data-test-subj="indexResultBadge"
|
||||
color={getIndexResultBadgeColor(incompatible)}
|
||||
{...props}
|
||||
>
|
||||
{incompatible > 0 ? i18n.FAIL : i18n.PASS}
|
||||
</StyledBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
IndexResultBadgeComponent.displayName = 'IndexCheckResultBadgeComponent';
|
||||
|
||||
export const IndexResultBadge = React.memo(IndexResultBadgeComponent);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FAIL = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.fail',
|
||||
{
|
||||
defaultMessage: 'Fail',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.indexResultBadge.pass',
|
||||
{
|
||||
defaultMessage: 'Pass',
|
||||
}
|
||||
);
|
|
@ -28,9 +28,9 @@ import { mockIlmExplain } from '../../mock/ilm_explain/mock_ilm_explain';
|
|||
import { mockDataQualityCheckResult } from '../../mock/data_quality_check_result/mock_index';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { mockStats } from '../../mock/stats/mock_stats';
|
||||
import { IndexSummaryTableItem } from '../summary_table/helpers';
|
||||
import { DataQualityCheckResult } from '../../types';
|
||||
import { getIndexNames, getTotalDocsCount } from '../../helpers';
|
||||
import { IndexSummaryTableItem } from './types';
|
||||
|
||||
const hot: IlmExplainLifecycleLifecycleExplainManaged = {
|
||||
index: '.ds-packetbeat-8.6.1-2023.02.04-000001',
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import type { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { isEqual, orderBy } from 'lodash/fp';
|
||||
|
||||
import type { IndexSummaryTableItem } from '../summary_table/helpers';
|
||||
import type {
|
||||
IlmPhase,
|
||||
IlmExplainPhaseCounts,
|
||||
|
@ -18,6 +17,7 @@ import type {
|
|||
MeteringStatsIndex,
|
||||
} from '../../types';
|
||||
import { getDocsCount, getSizeInBytes } from '../../helpers';
|
||||
import { IndexSummaryTableItem } from './types';
|
||||
|
||||
export const isManaged = (
|
||||
ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined
|
||||
|
|
|
@ -5,15 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { EMPTY_STAT } from '../../helpers';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import { Pattern } from '.';
|
||||
import { getCheckState } from '../../stub/get_check_state';
|
||||
|
||||
const indexName = 'auditbeat-custom-index-1';
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT;
|
||||
|
@ -38,23 +42,14 @@ jest.mock('../../use_ilm_explain', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
const ilmPhases = ['hot', 'warm', 'unmanaged'];
|
||||
|
||||
const defaultProps: ComponentProps<typeof Pattern> = {
|
||||
pattern: '',
|
||||
addSuccessToast: jest.fn(),
|
||||
canUserCreateAndReadCases: jest.fn(),
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick: jest.fn(),
|
||||
ilmPhases: ['hot', 'warm', 'unmanaged'],
|
||||
indexNames: undefined,
|
||||
isAssistantEnabled: true,
|
||||
openCreateCaseFlyout: jest.fn(),
|
||||
patternRollup: undefined,
|
||||
selectedIndex: null,
|
||||
setSelectedIndex: jest.fn(),
|
||||
baseTheme: DARK_THEME,
|
||||
updatePatternIndexNames: jest.fn(),
|
||||
updatePatternRollup: jest.fn(),
|
||||
chartSelectedIndex: null,
|
||||
setChartSelectedIndex: jest.fn(),
|
||||
indexNames: undefined,
|
||||
};
|
||||
|
||||
describe('pattern', () => {
|
||||
|
@ -66,9 +61,20 @@ describe('pattern', () => {
|
|||
const pattern = 'remote:*'; // <-- a colon in the pattern indicates the use of cross cluster search
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Pattern {...defaultProps} pattern={pattern} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
ilmPhases,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: getCheckState(indexName),
|
||||
}}
|
||||
>
|
||||
<Pattern {...defaultProps} pattern={pattern} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('remoteClustersCallout')).toBeInTheDocument();
|
||||
|
@ -78,9 +84,20 @@ describe('pattern', () => {
|
|||
const pattern = 'auditbeat-*'; // <-- no colon in the pattern
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Pattern {...defaultProps} pattern={pattern} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
dataQualityContextProps={{
|
||||
ilmPhases,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
}}
|
||||
indicesCheckContextProps={{
|
||||
checkState: getCheckState(indexName),
|
||||
}}
|
||||
>
|
||||
<Pattern {...defaultProps} pattern={pattern} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('remoteClustersCallout')).not.toBeInTheDocument();
|
||||
|
|
|
@ -5,26 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FlameElementEvent,
|
||||
HeatmapElementEvent,
|
||||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiSpacer, useGeneratedHtmlId } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ErrorEmptyPrompt } from '../error_empty_prompt';
|
||||
import {
|
||||
defaultSort,
|
||||
getIlmExplainPhaseCounts,
|
||||
getIlmPhase,
|
||||
getPageIndex,
|
||||
getSummaryTableItems,
|
||||
MIN_PAGE_SIZE,
|
||||
|
@ -32,15 +19,12 @@ import {
|
|||
shouldCreatePatternRollup,
|
||||
} from './helpers';
|
||||
import {
|
||||
getDocsCount,
|
||||
getIndexId,
|
||||
getIndexNames,
|
||||
getTotalDocsCount,
|
||||
getTotalPatternIncompatible,
|
||||
getTotalPatternIndicesChecked,
|
||||
getTotalSizeInBytes,
|
||||
} from '../../helpers';
|
||||
import { IndexProperties } from '../index_properties';
|
||||
import { LoadingEmptyPrompt } from '../loading_empty_prompt';
|
||||
import { PatternSummary } from './pattern_summary';
|
||||
import { RemoteClustersCallout } from '../remote_clusters_callout';
|
||||
|
@ -51,86 +35,41 @@ import type { PatternRollup, SelectedIndex, SortConfig } from '../../types';
|
|||
import { useIlmExplain } from '../../use_ilm_explain';
|
||||
import { useStats } from '../../use_stats';
|
||||
import { useDataQualityContext } from '../data_quality_context';
|
||||
|
||||
const IndexPropertiesContainer = styled.div`
|
||||
margin-bottom: ${euiThemeVars.euiSizeS};
|
||||
width: 100%;
|
||||
`;
|
||||
import { PatternAccordion, PatternAccordionChildren } from './styles';
|
||||
import { IndexCheckFlyout } from './index_check_flyout';
|
||||
import { useResultsRollupContext } from '../../contexts/results_rollup_context';
|
||||
import { useIndicesCheckContext } from '../../contexts/indices_check_context';
|
||||
|
||||
const EMPTY_INDEX_NAMES: string[] = [];
|
||||
|
||||
interface Props {
|
||||
addSuccessToast: (toast: { title: string }) => void;
|
||||
baseTheme: Theme;
|
||||
canUserCreateAndReadCases: () => boolean;
|
||||
endDate?: string | null;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
getGroupByFieldsOnClick: (
|
||||
elements: Array<
|
||||
| FlameElementEvent
|
||||
| HeatmapElementEvent
|
||||
| MetricElementEvent
|
||||
| PartitionElementEvent
|
||||
| WordCloudElementEvent
|
||||
| XYChartElementEvent
|
||||
>
|
||||
) => {
|
||||
groupByField0: string;
|
||||
groupByField1: string;
|
||||
};
|
||||
ilmPhases: string[];
|
||||
indexNames: string[] | undefined;
|
||||
isAssistantEnabled: boolean;
|
||||
openCreateCaseFlyout: ({
|
||||
comments,
|
||||
headerContent,
|
||||
}: {
|
||||
comments: string[];
|
||||
headerContent?: React.ReactNode;
|
||||
}) => void;
|
||||
pattern: string;
|
||||
patternRollup: PatternRollup | undefined;
|
||||
selectedIndex: SelectedIndex | null;
|
||||
setSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
|
||||
startDate?: string | null;
|
||||
theme?: PartialTheme;
|
||||
updatePatternIndexNames: ({
|
||||
indexNames,
|
||||
pattern,
|
||||
}: {
|
||||
indexNames: string[];
|
||||
pattern: string;
|
||||
}) => void;
|
||||
updatePatternRollup: (patternRollup: PatternRollup, requestTime?: number) => void;
|
||||
chartSelectedIndex: SelectedIndex | null;
|
||||
setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
|
||||
}
|
||||
|
||||
const PatternComponent: React.FC<Props> = ({
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
endDate,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getGroupByFieldsOnClick,
|
||||
indexNames,
|
||||
ilmPhases,
|
||||
isAssistantEnabled,
|
||||
openCreateCaseFlyout,
|
||||
pattern,
|
||||
patternRollup,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
startDate,
|
||||
theme,
|
||||
baseTheme,
|
||||
updatePatternIndexNames,
|
||||
updatePatternRollup,
|
||||
chartSelectedIndex,
|
||||
setChartSelectedIndex,
|
||||
}) => {
|
||||
const { httpFetch, isILMAvailable, ilmPhases, startDate, endDate, formatBytes, formatNumber } =
|
||||
useDataQualityContext();
|
||||
const { checkIndex, checkState } = useIndicesCheckContext();
|
||||
const { updatePatternIndexNames, updatePatternRollup } = useResultsRollupContext();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const [sorting, setSorting] = useState<SortConfig>(defaultSort);
|
||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
||||
const [pageSize, setPageSize] = useState<number>(MIN_PAGE_SIZE);
|
||||
const patternComponentAccordionId = useGeneratedHtmlId({ prefix: 'patternComponentAccordion' });
|
||||
const [expandedIndexName, setExpandedIndexName] = useState<string | null>(null);
|
||||
const flyoutIndexExpandActionAbortControllerRef = useRef(new AbortController());
|
||||
const tableRowIndexCheckNowActionAbortControllerRef = useRef(new AbortController());
|
||||
const flyoutIndexChartSelectedActionAbortControllerRef = useRef(new AbortController());
|
||||
|
||||
const {
|
||||
error: statsError,
|
||||
|
@ -145,70 +84,13 @@ const PatternComponent: React.FC<Props> = ({
|
|||
);
|
||||
const error = useMemo(() => statsError ?? ilmExplainError, [ilmExplainError, statsError]);
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<
|
||||
Record<string, React.ReactNode>
|
||||
>({});
|
||||
|
||||
const toggleExpanded = useCallback(
|
||||
(indexName: string) => {
|
||||
if (itemIdToExpandedRowMap[indexName]) {
|
||||
setItemIdToExpandedRowMap({});
|
||||
} else {
|
||||
setItemIdToExpandedRowMap({
|
||||
[indexName]: (
|
||||
<IndexPropertiesContainer>
|
||||
<IndexProperties
|
||||
addSuccessToast={addSuccessToast}
|
||||
canUserCreateAndReadCases={canUserCreateAndReadCases}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
docsCount={getDocsCount({ stats, indexName })}
|
||||
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
|
||||
ilmPhase={
|
||||
isILMAvailable && ilmExplain != null
|
||||
? getIlmPhase(ilmExplain?.[indexName], isILMAvailable)
|
||||
: undefined
|
||||
}
|
||||
indexId={getIndexId({ stats, indexName })}
|
||||
indexName={indexName}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
openCreateCaseFlyout={openCreateCaseFlyout}
|
||||
pattern={pattern}
|
||||
patternRollup={patternRollup}
|
||||
theme={theme}
|
||||
baseTheme={baseTheme}
|
||||
updatePatternRollup={updatePatternRollup}
|
||||
/>
|
||||
</IndexPropertiesContainer>
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
itemIdToExpandedRowMap,
|
||||
addSuccessToast,
|
||||
canUserCreateAndReadCases,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
stats,
|
||||
getGroupByFieldsOnClick,
|
||||
ilmExplain,
|
||||
isILMAvailable,
|
||||
isAssistantEnabled,
|
||||
openCreateCaseFlyout,
|
||||
pattern,
|
||||
patternRollup,
|
||||
theme,
|
||||
baseTheme,
|
||||
updatePatternRollup,
|
||||
]
|
||||
);
|
||||
|
||||
const ilmExplainPhaseCounts = useMemo(
|
||||
() => (isILMAvailable ? getIlmExplainPhaseCounts(ilmExplain) : undefined),
|
||||
[ilmExplain, isILMAvailable]
|
||||
);
|
||||
|
||||
const isFlyoutVisible = expandedIndexName !== null;
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
getSummaryTableItems({
|
||||
|
@ -235,6 +117,39 @@ const PatternComponent: React.FC<Props> = ({
|
|||
]
|
||||
);
|
||||
|
||||
const handleFlyoutClose = useCallback(() => {
|
||||
setExpandedIndexName(null);
|
||||
}, []);
|
||||
|
||||
const handleFlyoutIndexExpandAction = useCallback(
|
||||
(indexName) => {
|
||||
checkIndex({
|
||||
abortController: flyoutIndexExpandActionAbortControllerRef.current,
|
||||
indexName,
|
||||
pattern,
|
||||
httpFetch,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
});
|
||||
setExpandedIndexName(indexName);
|
||||
},
|
||||
[checkIndex, formatBytes, formatNumber, httpFetch, pattern]
|
||||
);
|
||||
|
||||
const handleTableRowIndexCheckNowAction = useCallback(
|
||||
(indexName) => {
|
||||
checkIndex({
|
||||
abortController: tableRowIndexCheckNowActionAbortControllerRef.current,
|
||||
indexName,
|
||||
pattern,
|
||||
httpFetch,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
});
|
||||
},
|
||||
[checkIndex, formatBytes, formatNumber, httpFetch, pattern]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newIndexNames = getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable });
|
||||
const newDocsCount = getTotalDocsCount({ indexNames: newIndexNames, stats });
|
||||
|
@ -296,9 +211,9 @@ const PatternComponent: React.FC<Props> = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex?.pattern === pattern) {
|
||||
if (chartSelectedIndex?.pattern === pattern) {
|
||||
const selectedPageIndex = getPageIndex({
|
||||
indexName: selectedIndex.indexName,
|
||||
indexName: chartSelectedIndex.indexName,
|
||||
items,
|
||||
pageSize,
|
||||
});
|
||||
|
@ -307,30 +222,57 @@ const PatternComponent: React.FC<Props> = ({
|
|||
setPageIndex(selectedPageIndex);
|
||||
}
|
||||
|
||||
if (itemIdToExpandedRowMap[selectedIndex.indexName] == null) {
|
||||
toggleExpanded(selectedIndex.indexName); // expand the selected index
|
||||
if (chartSelectedIndex.indexName !== expandedIndexName && !isFlyoutVisible) {
|
||||
checkIndex({
|
||||
abortController: flyoutIndexChartSelectedActionAbortControllerRef.current,
|
||||
indexName: chartSelectedIndex.indexName,
|
||||
pattern: chartSelectedIndex.pattern,
|
||||
httpFetch,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
});
|
||||
setExpandedIndexName(chartSelectedIndex.indexName);
|
||||
}
|
||||
|
||||
containerRef.current?.scrollIntoView();
|
||||
setSelectedIndex(null);
|
||||
setChartSelectedIndex(null);
|
||||
}
|
||||
}, [
|
||||
itemIdToExpandedRowMap,
|
||||
items,
|
||||
pageSize,
|
||||
pattern,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
toggleExpanded,
|
||||
chartSelectedIndex,
|
||||
setChartSelectedIndex,
|
||||
expandedIndexName,
|
||||
isFlyoutVisible,
|
||||
checkIndex,
|
||||
httpFetch,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const flyoutIndexExpandActionAbortController =
|
||||
flyoutIndexExpandActionAbortControllerRef.current;
|
||||
const tableRowIndexCheckNowActionAbortController =
|
||||
tableRowIndexCheckNowActionAbortControllerRef.current;
|
||||
const flyoutIndexChartSelectedActionAbortController =
|
||||
flyoutIndexChartSelectedActionAbortControllerRef.current;
|
||||
return () => {
|
||||
flyoutIndexExpandActionAbortController.abort();
|
||||
tableRowIndexCheckNowActionAbortController.abort();
|
||||
flyoutIndexChartSelectedActionAbortController.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPanel data-test-subj={`${pattern}PatternPanel`} hasBorder={false} hasShadow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj={`${pattern}PatternPanel`}>
|
||||
<PatternAccordion
|
||||
id={patternComponentAccordionId}
|
||||
initialIsOpen={true}
|
||||
buttonElement="div"
|
||||
buttonContent={
|
||||
<PatternSummary
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
incompatible={getTotalPatternIncompatible(patternRollup?.results)}
|
||||
indices={indexNames?.length}
|
||||
indicesChecked={getTotalPatternIndicesChecked(patternRollup)}
|
||||
|
@ -339,44 +281,64 @@ const PatternComponent: React.FC<Props> = ({
|
|||
patternDocsCount={patternRollup?.docsCount ?? 0}
|
||||
patternSizeInBytes={patternRollup?.sizeInBytes}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
}
|
||||
>
|
||||
<PatternAccordionChildren>
|
||||
{!loading && pattern.includes(':') && (
|
||||
<>
|
||||
<RemoteClustersCallout />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && pattern.includes(':') && (
|
||||
<>
|
||||
<RemoteClustersCallout />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{!loading && error != null && (
|
||||
<>
|
||||
<ErrorEmptyPrompt title={i18n.ERROR_LOADING_METADATA_TITLE(pattern)} />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && error != null && (
|
||||
<ErrorEmptyPrompt title={i18n.ERROR_LOADING_METADATA_TITLE(pattern)} />
|
||||
)}
|
||||
{loading && (
|
||||
<>
|
||||
<LoadingEmptyPrompt loading={i18n.LOADING_STATS} />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{loading && <LoadingEmptyPrompt loading={i18n.LOADING_STATS} />}
|
||||
|
||||
{!loading && error == null && (
|
||||
<div ref={containerRef}>
|
||||
<SummaryTable
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
getTableColumns={getSummaryTableColumns}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
items={items}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
pattern={pattern}
|
||||
setPageIndex={setPageIndex}
|
||||
setPageSize={setPageSize}
|
||||
setSorting={setSorting}
|
||||
toggleExpanded={toggleExpanded}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{!loading && error == null && (
|
||||
<div ref={containerRef}>
|
||||
<SummaryTable
|
||||
getTableColumns={getSummaryTableColumns}
|
||||
checkState={checkState}
|
||||
items={items}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
pattern={pattern}
|
||||
setPageIndex={setPageIndex}
|
||||
setPageSize={setPageSize}
|
||||
setSorting={setSorting}
|
||||
onExpandAction={handleFlyoutIndexExpandAction}
|
||||
onCheckNowAction={handleTableRowIndexCheckNowAction}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PatternAccordionChildren>
|
||||
</PatternAccordion>
|
||||
{isFlyoutVisible ? (
|
||||
<IndexCheckFlyout
|
||||
pattern={pattern}
|
||||
indexName={expandedIndexName}
|
||||
patternRollup={patternRollup}
|
||||
ilmExplain={ilmExplain}
|
||||
stats={stats}
|
||||
onClose={handleFlyoutClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PatternComponent.displayName = 'PatternComponent';
|
||||
|
||||
export const Pattern = React.memo(PatternComponent);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import moment from 'moment';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { IndexCheckFlyout } from '.';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { mockIlmExplain } from '../../../mock/ilm_explain/mock_ilm_explain';
|
||||
import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { mockStats } from '../../../mock/stats/mock_stats';
|
||||
|
||||
describe('IndexCheckFlyout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render heading section correctly with formatted latest check time', () => {
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'auditbeat-custom-index-1'
|
||||
);
|
||||
expect(screen.getByTestId('latestCheckedAt')).toHaveTextContent(
|
||||
moment(auditbeatWithAllResults.results!['auditbeat-custom-index-1'].checkedAt).format(
|
||||
'MMM DD, YYYY @ HH:mm:ss.SSS'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the correct index properties panel', () => {
|
||||
expect(screen.getByTestId('indexStatsPanel')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('indexCheckFields')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render footer with check now button', () => {
|
||||
expect(screen.getByRole('button', { name: 'Check now' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when flyout close is clicked', () => {
|
||||
it('should call onClose', () => {
|
||||
const onClose = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={onClose}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close this dialog' });
|
||||
userEvent.click(closeButton);
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check now button is clicked', () => {
|
||||
it('should call checkIndex', () => {
|
||||
const checkIndex = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders
|
||||
indicesCheckContextProps={{
|
||||
checkIndex,
|
||||
}}
|
||||
>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const checkNowButton = screen.getByRole('button', { name: 'Check now' });
|
||||
userEvent.click(checkNowButton);
|
||||
|
||||
expect(checkIndex).toHaveBeenCalledWith({
|
||||
abortController: expect.any(AbortController),
|
||||
formatBytes: expect.any(Function),
|
||||
formatNumber: expect.any(Function),
|
||||
httpFetch: expect.any(Function),
|
||||
indexName: 'auditbeat-custom-index-1',
|
||||
pattern: 'auditbeat-*',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import moment from 'moment';
|
||||
import { useIndicesCheckContext } from '../../../contexts/indices_check_context';
|
||||
|
||||
import { EMPTY_STAT, getDocsCount, getSizeInBytes } from '../../../helpers';
|
||||
import { MeteringStatsIndex, PatternRollup } from '../../../types';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { IndexProperties } from '../../index_properties';
|
||||
import { getIlmPhase } from '../helpers';
|
||||
import { IndexResultBadge } from '../../index_result_badge';
|
||||
import { useCurrentWindowWidth } from '../../../use_current_window_width';
|
||||
import { CHECK_NOW } from './translations';
|
||||
|
||||
export interface Props {
|
||||
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
|
||||
indexName: string;
|
||||
pattern: string;
|
||||
patternRollup: PatternRollup | undefined;
|
||||
stats: Record<string, MeteringStatsIndex> | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
||||
ilmExplain,
|
||||
indexName,
|
||||
pattern,
|
||||
patternRollup,
|
||||
stats,
|
||||
onClose,
|
||||
}) => {
|
||||
const currentWindowWidth = useCurrentWindowWidth();
|
||||
const isLargeScreen = currentWindowWidth > 1720;
|
||||
const isMediumScreen = currentWindowWidth > 1200;
|
||||
const { httpFetch, formatBytes, formatNumber, isILMAvailable } = useDataQualityContext();
|
||||
const { checkState, checkIndex } = useIndicesCheckContext();
|
||||
const indexCheckState = checkState[indexName];
|
||||
const isChecking = indexCheckState?.isChecking ?? false;
|
||||
const partitionedFieldMetadata = indexCheckState?.partitionedFieldMetadata ?? null;
|
||||
const indexResult = patternRollup?.results?.[indexName];
|
||||
const indexCheckFlyoutTitleId = useGeneratedHtmlId({
|
||||
prefix: 'indexCheckFlyoutTitle',
|
||||
});
|
||||
const abortControllerRef = React.useRef(new AbortController());
|
||||
|
||||
const handleCheckNow = useCallback(() => {
|
||||
checkIndex({
|
||||
abortController: abortControllerRef.current,
|
||||
indexName,
|
||||
pattern,
|
||||
httpFetch,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
});
|
||||
}, [checkIndex, formatBytes, formatNumber, httpFetch, indexName, pattern]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = abortControllerRef.current;
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-test-subj="indexCheckFlyout">
|
||||
<EuiFlyout
|
||||
size={isLargeScreen ? '50%' : isMediumScreen ? '70%' : '90%'}
|
||||
ownFocus
|
||||
onClose={onClose}
|
||||
aria-labelledby={indexCheckFlyoutTitleId}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
{partitionedFieldMetadata?.incompatible != null && (
|
||||
<IndexResultBadge incompatible={partitionedFieldMetadata.incompatible.length} />
|
||||
)}
|
||||
<h2 id={indexCheckFlyoutTitleId}>{indexName}</h2>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
{indexResult != null && indexResult.checkedAt != null && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s" data-test-subj="latestCheckedAt">
|
||||
{moment(indexResult.checkedAt).isValid()
|
||||
? moment(indexResult.checkedAt).format('MMM DD, YYYY @ HH:mm:ss.SSS')
|
||||
: EMPTY_STAT}
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<IndexProperties
|
||||
docsCount={getDocsCount({ stats, indexName })}
|
||||
sizeInBytes={getSizeInBytes({ stats, indexName })}
|
||||
ilmPhase={
|
||||
isILMAvailable && ilmExplain != null
|
||||
? getIlmPhase(ilmExplain?.[indexName], isILMAvailable)
|
||||
: undefined
|
||||
}
|
||||
indexName={indexName}
|
||||
pattern={pattern}
|
||||
patternRollup={patternRollup}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton iconType="refresh" isLoading={isChecking} onClick={handleCheckNow} fill>
|
||||
{CHECK_NOW}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
IndexCheckFlyoutComponent.displayName = 'IndexCheckFlyoutComponent';
|
||||
|
||||
export const IndexCheckFlyout = React.memo(IndexCheckFlyoutComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CHECK_NOW: string = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.checkNowButton',
|
||||
{
|
||||
defaultMessage: 'Check now',
|
||||
}
|
||||
);
|
|
@ -13,8 +13,6 @@ import { PatternLabel } from './pattern_label';
|
|||
import { StatsRollup } from './stats_rollup';
|
||||
|
||||
interface Props {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
ilmExplainPhaseCounts: IlmExplainPhaseCounts | undefined;
|
||||
incompatible: number | undefined;
|
||||
indices: number | undefined;
|
||||
|
@ -25,8 +23,6 @@ interface Props {
|
|||
}
|
||||
|
||||
const PatternSummaryComponent: React.FC<Props> = ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
ilmExplainPhaseCounts,
|
||||
incompatible,
|
||||
indices,
|
||||
|
@ -35,7 +31,7 @@ const PatternSummaryComponent: React.FC<Props> = ({
|
|||
patternDocsCount,
|
||||
patternSizeInBytes,
|
||||
}) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexGroup wrap={true} alignItems="center" gutterSize="xs" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PatternLabel
|
||||
incompatible={incompatible}
|
||||
|
@ -49,8 +45,6 @@ const PatternSummaryComponent: React.FC<Props> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<StatsRollup
|
||||
docsCount={patternDocsCount}
|
||||
formatBytes={formatBytes}
|
||||
formatNumber={formatNumber}
|
||||
incompatible={incompatible}
|
||||
indices={indices}
|
||||
indicesChecked={indicesChecked}
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getResultToolTip, showResult } from './helpers';
|
||||
import { getPatternResultTooltip, showResult } from './helpers';
|
||||
import { ALL_PASSED, SOME_FAILED, SOME_UNCHECKED } from './translations';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getResultToolTip', () => {
|
||||
describe('getPatternResultTooltip', () => {
|
||||
test('it returns the expected tool tip when `incompatible` is undefined', () => {
|
||||
expect(getResultToolTip(undefined)).toEqual(SOME_UNCHECKED);
|
||||
expect(getPatternResultTooltip(undefined)).toEqual(SOME_UNCHECKED);
|
||||
});
|
||||
|
||||
test('it returns the expected tool tip when `incompatible` is zero', () => {
|
||||
expect(getResultToolTip(0)).toEqual(ALL_PASSED);
|
||||
expect(getPatternResultTooltip(0)).toEqual(ALL_PASSED);
|
||||
});
|
||||
|
||||
test('it returns the expected tool tip when `incompatible` is non-zero', () => {
|
||||
expect(getResultToolTip(1)).toEqual(SOME_FAILED);
|
||||
expect(getPatternResultTooltip(1)).toEqual(SOME_FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const getResultToolTip = (incompatible: number | undefined): string => {
|
||||
export const getPatternResultTooltip = (incompatible: number | undefined): string => {
|
||||
if (incompatible == null) {
|
||||
return i18n.SOME_UNCHECKED;
|
||||
} else if (incompatible === 0) {
|
||||
|
@ -16,14 +16,16 @@ export const getResultToolTip = (incompatible: number | undefined): string => {
|
|||
return i18n.SOME_FAILED;
|
||||
}
|
||||
};
|
||||
interface ShowResultProps<T> {
|
||||
incompatible: T;
|
||||
indices: T;
|
||||
indicesChecked: T;
|
||||
}
|
||||
|
||||
export const showResult = ({
|
||||
incompatible,
|
||||
indices,
|
||||
indicesChecked,
|
||||
}: {
|
||||
incompatible: number | undefined;
|
||||
indices: number | undefined;
|
||||
indicesChecked: number | undefined;
|
||||
}): boolean =>
|
||||
incompatible != null && indices != null && indicesChecked != null && indices === indicesChecked;
|
||||
export const showResult = <T extends number | undefined>(
|
||||
opts: ShowResultProps<T>
|
||||
): opts is ShowResultProps<NonNullable<T>> =>
|
||||
opts.incompatible != null &&
|
||||
opts.indices != null &&
|
||||
opts.indicesChecked != null &&
|
||||
opts.indices === opts.indicesChecked;
|
||||
|
|
|
@ -5,19 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiToolTip, EuiIcon } from '@elastic/eui';
|
||||
import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getResultToolTip, showResult } from './helpers';
|
||||
import { getPatternResultTooltip, showResult } from './helpers';
|
||||
import { IlmPhaseCounts } from '../../../ilm_phase_counts';
|
||||
import { getResultIcon, getResultIconColor } from '../../../summary_table/helpers';
|
||||
import * as i18n from '../translations';
|
||||
import type { IlmExplainPhaseCounts } from '../../../../types';
|
||||
|
||||
const ResultContainer = styled.div`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
import { IndexResultBadge } from '../../../index_result_badge';
|
||||
|
||||
interface Props {
|
||||
incompatible: number | undefined;
|
||||
|
@ -33,41 +28,46 @@ const PatternLabelComponent: React.FC<Props> = ({
|
|||
indices,
|
||||
indicesChecked,
|
||||
pattern,
|
||||
}) => (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ResultContainer>
|
||||
{showResult({
|
||||
incompatible,
|
||||
indices,
|
||||
indicesChecked,
|
||||
}) && (
|
||||
<EuiToolTip content={getResultToolTip(incompatible)}>
|
||||
<EuiIcon
|
||||
color={getResultIconColor(incompatible)}
|
||||
type={getResultIcon(incompatible)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</ResultContainer>
|
||||
</EuiFlexItem>
|
||||
}) => {
|
||||
// this is a workaround for type guard limitation
|
||||
// TS does not type narrow value passed to the type guard function
|
||||
// if that value is proxied via another key like for example {incompatible: *incompatible*}
|
||||
// so we need a reference object to pass it to the type guard
|
||||
// and then check the keys of that object (resultOpts) for type guarded result
|
||||
// to be properly type narrowed instead
|
||||
const resultOpts = {
|
||||
incompatible,
|
||||
indices,
|
||||
indicesChecked,
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
|
||||
{showResult(resultOpts) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<IndexResultBadge
|
||||
incompatible={resultOpts.incompatible}
|
||||
tooltipText={getPatternResultTooltip(resultOpts.incompatible)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={i18n.PATTERN_OR_INDEX_TOOLTIP}>
|
||||
<EuiTitle size="s">
|
||||
<EuiTitle size="xxs">
|
||||
<h2>{pattern}</h2>
|
||||
</EuiTitle>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
{ilmExplainPhaseCounts && (
|
||||
<IlmPhaseCounts ilmExplainPhaseCounts={ilmExplainPhaseCounts} pattern={pattern} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{ilmExplainPhaseCounts && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<IlmPhaseCounts ilmExplainPhaseCounts={ilmExplainPhaseCounts} pattern={pattern} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
PatternLabelComponent.displayName = 'PatternLabelComponent';
|
||||
|
||||
|
|
|
@ -17,13 +17,14 @@ export const ALL_PASSED = i18n.translate(
|
|||
export const SOME_FAILED = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip',
|
||||
{
|
||||
defaultMessage: 'Some indices matching this pattern failed the data quality checks',
|
||||
defaultMessage: 'At least one index matching this pattern failed a data quality check',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOME_UNCHECKED = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip',
|
||||
{
|
||||
defaultMessage: 'Some indices matching this pattern have not been checked for data quality',
|
||||
defaultMessage:
|
||||
'At least one index matching this pattern has not been checked for data quality',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../../mock/test_providers/test_providers';
|
||||
import { StatsRollup } from '.';
|
||||
|
||||
describe('StatsRollup', () => {
|
||||
it('should render properly formatted stats rollup', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<StatsRollup
|
||||
docsCount={1111111}
|
||||
incompatible={2222222}
|
||||
indices={3333333}
|
||||
indicesChecked={4444444}
|
||||
sizeInBytes={5555555}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId('statsRollup');
|
||||
|
||||
expect(container).toHaveTextContent('Incompatible fields2,222,222');
|
||||
expect(container).toHaveTextContent('Indices checked4,444,444');
|
||||
expect(container).toHaveTextContent('Indices3,333,333');
|
||||
expect(container).toHaveTextContent('Size5.3MB');
|
||||
expect(container).toHaveTextContent('Docs1,111,111');
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['Docs', '--'],
|
||||
['Incompatible fields', '--'],
|
||||
['Indices', '0'],
|
||||
['Indices checked', '--'],
|
||||
])('when %s count is not provided', (statLabel, emptyText) => {
|
||||
it(`should render empty ${statLabel} stat`, () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<StatsRollup />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId('statsRollup');
|
||||
|
||||
expect(container).toHaveTextContent(`${statLabel}${emptyText}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when size count is not provided', () => {
|
||||
it('should not render size stat', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<StatsRollup
|
||||
docsCount={1111111}
|
||||
incompatible={2222222}
|
||||
indices={3333333}
|
||||
indicesChecked={4444444}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId('statsRollup');
|
||||
|
||||
expect(container).not.toHaveTextContent('Size');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[
|
||||
'Docs',
|
||||
'Total number of docs in indices matching this index pattern',
|
||||
'Total number of docs in all indices',
|
||||
],
|
||||
[
|
||||
'Incompatible fields',
|
||||
'Total number of checked fields incompatible with ECS in indices matching this index pattern',
|
||||
'Total number of checked fields incompatible with ECS',
|
||||
],
|
||||
['Indices', 'Total number of indices matching this index pattern', 'Total number of indices'],
|
||||
[
|
||||
'Indices checked',
|
||||
'Total number of checked indices matching this index pattern',
|
||||
'Total number of checked indices',
|
||||
],
|
||||
[
|
||||
'Size',
|
||||
'Total size of indices (excluding replicas) matching this index pattern',
|
||||
'Total size of indices (excluding replicas)',
|
||||
],
|
||||
])('%s count tooltips', (statLabelText, patternTooltipText, noPatternTooltipText) => {
|
||||
describe('when pattern is provided', () => {
|
||||
it('should render pattern specific tooltip', async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<StatsRollup
|
||||
docsCount={1111111}
|
||||
incompatible={2222222}
|
||||
indices={3333333}
|
||||
indicesChecked={4444444}
|
||||
pattern="my-pattern"
|
||||
sizeInBytes={5555555}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
userEvent.hover(screen.getByText(statLabelText));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||
patternTooltipText.replace('{pattern}', 'my-pattern')
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pattern is not provided', () => {
|
||||
it('should render default tooltip', async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<StatsRollup
|
||||
docsCount={1111111}
|
||||
incompatible={2222222}
|
||||
indices={3333333}
|
||||
indicesChecked={4444444}
|
||||
sizeInBytes={5555555}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
userEvent.hover(screen.getByText(statLabelText));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(noPatternTooltipText)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,152 +5,116 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EMPTY_STAT, getIncompatibleStatColor } from '../../../../helpers';
|
||||
import { StatLabel } from '../../../stat_label';
|
||||
import { EMPTY_STAT, getIncompatibleStatBadgeColor } from '../../../../helpers';
|
||||
import { useDataQualityContext } from '../../../data_quality_context';
|
||||
import * as i18n from '../../../stat_label/translations';
|
||||
import { Stat } from './stat';
|
||||
|
||||
const IndicesStatContainer = styled.div`
|
||||
min-width: 100px;
|
||||
const StyledStatWrapperFlexItem = styled(EuiFlexItem)`
|
||||
padding: 0 ${({ theme }) => theme.eui.euiSize};
|
||||
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-color: ${({ theme }) => theme.eui.euiBorderColor};
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
border-right: none;
|
||||
}
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const DocsContainer = styled.div`
|
||||
min-width: 155px;
|
||||
`;
|
||||
|
||||
const STAT_TITLE_SIZE = 's';
|
||||
|
||||
interface Props {
|
||||
docsCount: number | undefined;
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
incompatible: number | undefined;
|
||||
indices: number | undefined;
|
||||
indicesChecked: number | undefined;
|
||||
docsCount?: number;
|
||||
incompatible?: number;
|
||||
indices?: number;
|
||||
indicesChecked?: number;
|
||||
pattern?: string;
|
||||
sizeInBytes: number | undefined;
|
||||
sizeInBytes?: number;
|
||||
}
|
||||
|
||||
const StatsRollupComponent: React.FC<Props> = ({
|
||||
docsCount,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
incompatible,
|
||||
indices,
|
||||
indicesChecked,
|
||||
pattern,
|
||||
sizeInBytes,
|
||||
}) => {
|
||||
const incompatibleDescription = useMemo(
|
||||
() => <StatLabel line1={i18n.INCOMPATIBLE} line2={i18n.FIELDS} />,
|
||||
[]
|
||||
);
|
||||
const indicesCheckedDescription = useMemo(
|
||||
() => <StatLabel line1={i18n.INDICES} line2={i18n.CHECKED} />,
|
||||
[]
|
||||
);
|
||||
const sizeDescription = useMemo(() => <StatLabel line2={i18n.SIZE} />, []);
|
||||
const docsDescription = useMemo(() => <StatLabel line2={i18n.DOCS} />, []);
|
||||
const indicesDescription = useMemo(() => <StatLabel line2={i18n.INDICES} />, []);
|
||||
const { formatNumber, formatBytes } = useDataQualityContext();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
data-test-subj="statsRollup"
|
||||
gutterSize="s"
|
||||
gutterSize="none"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<StyledStatWrapperFlexItem grow={false}>
|
||||
<Stat
|
||||
tooltipText={
|
||||
pattern != null
|
||||
? i18n.INCOMPATIBLE_PATTERN_TOOL_TIP(pattern)
|
||||
? i18n.TOTAL_INCOMPATIBLE_PATTERN_TOOL_TIP
|
||||
: i18n.TOTAL_INCOMPATIBLE_TOOL_TIP
|
||||
}
|
||||
badgeText={incompatible != null ? formatNumber(incompatible) : EMPTY_STAT}
|
||||
badgeColor={getIncompatibleStatBadgeColor(incompatible)}
|
||||
>
|
||||
<EuiStat
|
||||
description={incompatibleDescription}
|
||||
title={incompatible != null ? formatNumber(incompatible) : EMPTY_STAT}
|
||||
titleColor={getIncompatibleStatColor(incompatible)}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{i18n.INCOMPATIBLE_FIELDS}
|
||||
</Stat>
|
||||
</StyledStatWrapperFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<IndicesStatContainer>
|
||||
<EuiToolTip
|
||||
content={
|
||||
pattern != null
|
||||
? i18n.TOTAL_COUNT_OF_INDICES_CHECKED_MATCHING_PATTERN_TOOL_TIP(pattern)
|
||||
: i18n.TOTAL_INDICES_CHECKED_TOOL_TIP
|
||||
}
|
||||
>
|
||||
<EuiStat
|
||||
description={indicesCheckedDescription}
|
||||
title={indicesChecked != null ? formatNumber(indicesChecked) : EMPTY_STAT}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</IndicesStatContainer>
|
||||
</EuiFlexItem>
|
||||
<StyledStatWrapperFlexItem grow={false}>
|
||||
<Stat
|
||||
tooltipText={
|
||||
pattern != null
|
||||
? i18n.TOTAL_CHECKED_INDICES_PATTERN_TOOL_TIP
|
||||
: i18n.TOTAL_CHECKED_INDICES_TOOL_TIP
|
||||
}
|
||||
badgeText={indicesChecked != null ? formatNumber(indicesChecked) : EMPTY_STAT}
|
||||
>
|
||||
{i18n.INDICES_CHECKED}
|
||||
</Stat>
|
||||
</StyledStatWrapperFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<IndicesStatContainer>
|
||||
<EuiToolTip
|
||||
content={
|
||||
pattern != null
|
||||
? i18n.TOTAL_COUNT_OF_INDICES_MATCHING_PATTERN_TOOL_TIP(pattern)
|
||||
: i18n.TOTAL_INDICES_TOOL_TIP
|
||||
}
|
||||
>
|
||||
<EuiStat
|
||||
description={indicesDescription}
|
||||
title={indices != null ? formatNumber(indices) : 0}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</IndicesStatContainer>
|
||||
</EuiFlexItem>
|
||||
<StyledStatWrapperFlexItem grow={false}>
|
||||
<Stat
|
||||
tooltipText={
|
||||
pattern != null ? i18n.TOTAL_INDICES_PATTERN_TOOL_TIP : i18n.TOTAL_INDICES_TOOL_TIP
|
||||
}
|
||||
badgeText={indices != null ? formatNumber(indices) : '0'}
|
||||
>
|
||||
{i18n.INDICES}
|
||||
</Stat>
|
||||
</StyledStatWrapperFlexItem>
|
||||
|
||||
{sizeInBytes != null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<IndicesStatContainer>
|
||||
<EuiToolTip
|
||||
content={
|
||||
pattern != null
|
||||
? i18n.INDICES_SIZE_PATTERN_TOOL_TIP(pattern)
|
||||
: i18n.TOTAL_SIZE_TOOL_TIP
|
||||
}
|
||||
>
|
||||
<EuiStat
|
||||
description={sizeDescription}
|
||||
title={sizeInBytes != null ? formatBytes(sizeInBytes) : EMPTY_STAT}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</IndicesStatContainer>
|
||||
</EuiFlexItem>
|
||||
<StyledStatWrapperFlexItem grow={false}>
|
||||
<Stat
|
||||
tooltipText={
|
||||
pattern != null ? i18n.TOTAL_SIZE_PATTERN_TOOL_TIP : i18n.TOTAL_SIZE_TOOL_TIP
|
||||
}
|
||||
badgeText={sizeInBytes != null ? formatBytes(sizeInBytes) : EMPTY_STAT}
|
||||
>
|
||||
{i18n.SIZE}
|
||||
</Stat>
|
||||
</StyledStatWrapperFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<DocsContainer>
|
||||
<EuiToolTip
|
||||
content={
|
||||
pattern != null ? i18n.INDEX_DOCS_PATTERN_TOOL_TIP(pattern) : i18n.TOTAL_DOCS_TOOL_TIP
|
||||
}
|
||||
>
|
||||
<EuiStat
|
||||
description={docsDescription}
|
||||
title={docsCount != null ? formatNumber(docsCount) : EMPTY_STAT}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</DocsContainer>
|
||||
</EuiFlexItem>
|
||||
<StyledStatWrapperFlexItem grow={false}>
|
||||
<Stat
|
||||
tooltipText={
|
||||
pattern != null ? i18n.TOTAL_DOCS_PATTERN_TOOL_TIP : i18n.TOTAL_DOCS_TOOL_TIP
|
||||
}
|
||||
badgeText={docsCount != null ? formatNumber(docsCount) : EMPTY_STAT}
|
||||
>
|
||||
{i18n.DOCS}
|
||||
</Stat>
|
||||
</StyledStatWrapperFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { Props, Stat, arePropsEqualOneLevelDeep } from '.';
|
||||
import { TestExternalProviders } from '../../../../../mock/test_providers/test_providers';
|
||||
|
||||
describe('Stat', () => {
|
||||
it('renders stat with badge', () => {
|
||||
render(<Stat badgeText="thebadge" badgeColor="hollow" />);
|
||||
expect(screen.getByTestId('stat')).toHaveTextContent('thebadge');
|
||||
});
|
||||
|
||||
it('renders stat with tooltip', async () => {
|
||||
render(<Stat badgeText="thebadge" badgeColor="hollow" tooltipText="thetooltip" />);
|
||||
userEvent.hover(screen.getByText('thebadge'));
|
||||
expect(screen.getByText('thebadge')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByText('thetooltip')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders stat with children', () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<Stat badgeText="thebadge" badgeColor="hollow">
|
||||
{'thechildren'}
|
||||
</Stat>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
expect(screen.getByText('thechildren')).toBeInTheDocument();
|
||||
expect(screen.getByText('thebadge')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('arePropsEqualOneLevelDeep', () => {
|
||||
describe('when badgeProps are equal', () => {
|
||||
it('returns true', () => {
|
||||
const prevProps = { badgeProps: { color: 'hollow' } } as Props;
|
||||
const nextProps = { badgeProps: { color: 'hollow' } } as Props;
|
||||
|
||||
expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when badgeProps are not equal', () => {
|
||||
it('returns false', () => {
|
||||
const prevProps = { badgeProps: { color: 'hollow' } } as Props;
|
||||
const nextProps = { badgeProps: { color: 'primary' } } as Props;
|
||||
|
||||
expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when other props are passed', () => {
|
||||
describe('when props are equal', () => {
|
||||
it('returns true', () => {
|
||||
const prevProps = { badgeText: '1' } as Props;
|
||||
const nextProps = { badgeText: '1' } as Props;
|
||||
|
||||
expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when props are not equal', () => {
|
||||
it('returns false', () => {
|
||||
const prevProps = { badgeText: '1' } as Props;
|
||||
const nextProps = { badgeText: '2' } as Props;
|
||||
|
||||
expect(arePropsEqualOneLevelDeep(prevProps, nextProps)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledText = styled(EuiText)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeS};
|
||||
vertical-align: baseline;
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
badgeText: string;
|
||||
badgeColor?: string;
|
||||
tooltipText?: string;
|
||||
children?: React.ReactNode;
|
||||
badgeProps?: React.ComponentProps<typeof EuiBadge>;
|
||||
}
|
||||
|
||||
const StatComponent: React.FC<Props> = ({
|
||||
badgeColor = 'hollow',
|
||||
badgeText,
|
||||
tooltipText,
|
||||
children,
|
||||
badgeProps,
|
||||
}) => {
|
||||
return (
|
||||
<EuiToolTip content={tooltipText}>
|
||||
<StyledText data-test-subj="stat" size={'xs'}>
|
||||
{children && <StyledDescription>{children}</StyledDescription>}
|
||||
<EuiBadge color={badgeColor} {...badgeProps}>
|
||||
{badgeText}
|
||||
</EuiBadge>
|
||||
</StyledText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
StatComponent.displayName = 'StatComponent';
|
||||
|
||||
// The badgeProps object requires a deeper level of comparison than the default shallow comparison.
|
||||
// However, using _.isEqual for this purpose would be excessive.
|
||||
// The other properties should continue to be checked shallowly.
|
||||
// In essence, only badgeProps needs a deeper comparison,
|
||||
// while the remaining properties can be compared using React's internal Object.is comparison.
|
||||
export const arePropsEqualOneLevelDeep = <T extends Props>(prevProps: T, nextProps: T): boolean => {
|
||||
for (const key of Object.keys(prevProps) as Array<keyof T>) {
|
||||
if (key === 'badgeProps') {
|
||||
const prevValue = prevProps[key];
|
||||
const nextValue = nextProps[key];
|
||||
if (prevValue && nextValue) {
|
||||
return arePropsEqualOneLevelDeep(
|
||||
prevValue as unknown as Props,
|
||||
nextValue as unknown as Props
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.is(prevProps[key], nextProps[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const Stat = React.memo(StatComponent, arePropsEqualOneLevelDeep);
|
|
@ -24,6 +24,6 @@ export const INDICES = i18n.translate(
|
|||
export const PATTERN_OR_INDEX_TOOLTIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip',
|
||||
{
|
||||
defaultMessage: 'A pattern or specific index',
|
||||
defaultMessage: 'Index name or pattern',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiAccordion } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const PatternAccordion = styled(EuiAccordion)`
|
||||
.euiAccordion__triggerWrapper {
|
||||
padding: 14px ${({ theme }) => theme.eui.euiSize};
|
||||
border: 1px solid ${({ theme }) => theme.eui.euiBorderColor};
|
||||
border-radius: ${({ theme }) => theme.eui.euiBorderRadius};
|
||||
}
|
||||
|
||||
.euiAccordion__button:is(:hover, :focus) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.euiAccordion__buttonContent {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PatternAccordionChildren = styled.div`
|
||||
padding: ${({ theme }) => theme.eui.euiSize};
|
||||
padding-bottom: 0;
|
||||
border: 1px solid ${({ theme }) => theme.eui.euiBorderColor};
|
||||
border-radius: 0 0 ${({ theme }) => `${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius}`};
|
||||
border-top: none;
|
||||
width: calc(100% - ${({ theme }) => theme.eui.euiSizeS} * 2);
|
||||
margin: auto;
|
||||
`;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IlmPhase } from '../../types';
|
||||
|
||||
export interface IndexSummaryTableItem {
|
||||
docsCount: number;
|
||||
incompatible: number | undefined;
|
||||
indexName: string;
|
||||
ilmPhase: IlmPhase | undefined;
|
||||
pattern: string;
|
||||
patternDocsCount: number;
|
||||
sizeInBytes: number | undefined;
|
||||
checkedAt: number | undefined;
|
||||
}
|
|
@ -9,15 +9,15 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { SAME_FAMILY } from './translations';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { SameFamily } from '.';
|
||||
|
||||
describe('SameFamily', () => {
|
||||
test('it renders a badge with the expected content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
<SameFamily />
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('sameFamily')).toHaveTextContent(SAME_FAMILY);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Line1 = styled.span`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const Line2 = styled.span`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const EMPTY = ' ';
|
||||
|
||||
interface Props {
|
||||
line1?: string;
|
||||
line2?: string;
|
||||
}
|
||||
|
||||
export const StatLabel: React.FC<Props> = ({ line1 = EMPTY, line2 = EMPTY }) => (
|
||||
<>
|
||||
<Line1 data-test-subj="line1">{line1}</Line1>
|
||||
<Line2 data-test-subj="line2">{line2}</Line2>
|
||||
</>
|
||||
);
|
|
@ -14,29 +14,6 @@ export const CHECKED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CUSTOM = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.customLabel',
|
||||
{
|
||||
defaultMessage: 'Custom',
|
||||
}
|
||||
);
|
||||
|
||||
export const CUSTOM_INDEX_TOOL_TIP = (indexName: string) =>
|
||||
i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.statLabels.customIndexToolTip', {
|
||||
values: { indexName },
|
||||
defaultMessage: 'A count of the custom field mappings in the {indexName} index',
|
||||
});
|
||||
|
||||
export const CUSTOM_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.customPatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage:
|
||||
'The total count of custom field mappings, in indices matching the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const DOCS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel',
|
||||
{
|
||||
|
@ -44,57 +21,20 @@ export const DOCS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const FIELDS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.fieldsLabel',
|
||||
export const SIZE = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel',
|
||||
{
|
||||
defaultMessage: 'fields',
|
||||
defaultMessage: 'Size',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCOMPATIBLE = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleLabel',
|
||||
export const INCOMPATIBLE_FIELDS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Incompatible',
|
||||
defaultMessage: 'Incompatible fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCOMPATIBLE_INDEX_TOOL_TIP = (indexName: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip',
|
||||
{
|
||||
values: { indexName },
|
||||
defaultMessage: 'Mappings and values incompatible with ECS, in the {indexName} index',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCOMPATIBLE_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatiblePatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage:
|
||||
'The total count of fields incompatible with ECS, in indices matching the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_DOCS_COUNT_TOOL_TIP = (indexName: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsCountToolTip',
|
||||
{
|
||||
values: { indexName },
|
||||
defaultMessage: 'A count of the docs in the {indexName} index',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_DOCS_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indexDocsPatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage: 'The total count of docs, in indices matching the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDICES = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel',
|
||||
{
|
||||
|
@ -102,6 +42,13 @@ export const INDICES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INDICES_CHECKED = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesCheckedLabel',
|
||||
{
|
||||
defaultMessage: 'Indices checked',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAME_FAMILY = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel',
|
||||
{
|
||||
|
@ -109,91 +56,80 @@ export const SAME_FAMILY = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SAME_FAMILY_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyPatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage:
|
||||
'The total count of fields in the same family as the type specified by ECS, in indices matching the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIZE = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel',
|
||||
export const INCOMPATIBLE_INDEX_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip',
|
||||
{
|
||||
defaultMessage: 'Size',
|
||||
defaultMessage: 'Mappings and values incompatible with ECS',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDICES_SIZE_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage:
|
||||
'The total size of the primary indices matching the {pattern} pattern (does not include replicas)',
|
||||
}
|
||||
);
|
||||
export const TOTAL_INCOMPATIBLE_PATTERN_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatiblePatternToolTip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Total number of checked fields incompatible with ECS in indices matching this index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_COUNT_OF_INDICES_CHECKED_MATCHING_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage: 'The total count of indices checked that match the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
export const TOTAL_DOCS_PATTERN_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsPatternToolTip',
|
||||
{
|
||||
defaultMessage: 'Total number of docs in indices matching this index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_COUNT_OF_INDICES_MATCHING_PATTERN_TOOL_TIP = (pattern: string) =>
|
||||
i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCountOfIndicesMatchingPatternToolTip',
|
||||
{
|
||||
values: { pattern },
|
||||
defaultMessage: 'The total count of indices matching the {pattern} pattern',
|
||||
}
|
||||
);
|
||||
export const TOTAL_SIZE_PATTERN_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizePatternToolTip',
|
||||
{
|
||||
defaultMessage: 'Total size of indices (excluding replicas) matching this index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_CHECKED_INDICES_PATTERN_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesPatternToolTip',
|
||||
{
|
||||
defaultMessage: 'Total number of checked indices matching this index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_INDICES_PATTERN_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesPatternToolTip',
|
||||
{
|
||||
defaultMessage: 'Total number of indices matching this index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_DOCS_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip',
|
||||
{
|
||||
defaultMessage: 'The total count of docs, in all indices',
|
||||
defaultMessage: 'Total number of docs in all indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_INCOMPATIBLE_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The total count of fields incompatible with ECS, in all indices that were checked',
|
||||
defaultMessage: 'Total number of checked fields incompatible with ECS',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_INDICES_CHECKED_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesCheckedToolTip',
|
||||
export const TOTAL_CHECKED_INDICES_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesToolTip',
|
||||
{
|
||||
defaultMessage: 'The total count of all indices checked',
|
||||
defaultMessage: 'Total number of checked indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_INDICES_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip',
|
||||
{
|
||||
defaultMessage: 'The total count of all indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_SAME_FAMILY_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSameFamilyToolTip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The total count of fields in the same family as the ECS type, in all indices that were checked',
|
||||
defaultMessage: 'Total number of indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_SIZE_TOOL_TIP = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip',
|
||||
{
|
||||
defaultMessage: 'The total size of all primary indices (does not include replicas)',
|
||||
defaultMessage: 'Total size of indices (excluding replicas)',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,14 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { ChartLegendLink } from '../../data_quality_panel/tabs/styles';
|
||||
import { FixedWidthLegendText } from '../../styles';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DEFAULT_DATA_TEST_SUBJ = 'chartLegendItem';
|
||||
|
||||
export const ChartLegendLink = styled(EuiLink)`
|
||||
width: 100%;
|
||||
`;
|
||||
export const FixedWidthLegendText = styled(EuiText)<{
|
||||
$width: number | undefined;
|
||||
}>`
|
||||
text-align: left;
|
||||
${({ $width }) => ($width != null ? `width: ${$width}px;` : '')}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
color: string | null;
|
||||
count: number | string;
|
|
@ -20,7 +20,10 @@ import { EMPTY_STAT } from '../../helpers';
|
|||
import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import type { Props } from '.';
|
||||
import { StorageTreemap } from '.';
|
||||
import { DEFAULT_MAX_CHART_HEIGHT } from '../tabs/styles';
|
||||
|
@ -54,8 +57,6 @@ const defaultProps: Props = {
|
|||
maxChartHeight: DEFAULT_MAX_CHART_HEIGHT,
|
||||
onIndexSelected,
|
||||
patternRollups,
|
||||
patterns,
|
||||
baseTheme: DARK_THEME,
|
||||
valueFormatter: formatBytes,
|
||||
};
|
||||
|
||||
|
@ -73,9 +74,11 @@ describe('StorageTreemap', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<StorageTreemap {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ patterns, baseTheme: DARK_THEME }}>
|
||||
<StorageTreemap {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -135,9 +138,11 @@ describe('StorageTreemap', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<StorageTreemap {...defaultProps} flattenedBuckets={emptyFlattenedBuckets} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ patterns, baseTheme: DARK_THEME }}>
|
||||
<StorageTreemap {...defaultProps} flattenedBuckets={emptyFlattenedBuckets} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
|||
MetricElementEvent,
|
||||
PartialTheme,
|
||||
PartitionElementEvent,
|
||||
Theme,
|
||||
WordCloudElementEvent,
|
||||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
|
@ -29,10 +28,11 @@ import {
|
|||
getLegendItems,
|
||||
getPathToFlattenedBucketMap,
|
||||
} from '../body/data_quality_details/storage_details/helpers';
|
||||
import { ChartLegendItem } from '../../ecs_summary_donut_chart/chart_legend/chart_legend_item';
|
||||
import { ChartLegendItem } from './chart_legend_item';
|
||||
import { NoData } from './no_data';
|
||||
import { ChartFlexItem, LegendContainer } from '../tabs/styles';
|
||||
import { PatternRollup, SelectedIndex } from '../../types';
|
||||
import { useDataQualityContext } from '../data_quality_context';
|
||||
|
||||
export const DEFAULT_MIN_CHART_HEIGHT = 240; // px
|
||||
export const LEGEND_WIDTH = 220; // px
|
||||
|
@ -40,14 +40,11 @@ export const LEGEND_TEXT_WITH = 120; // px
|
|||
|
||||
export interface Props {
|
||||
accessor: 'sizeInBytes' | 'docsCount';
|
||||
baseTheme: Theme;
|
||||
flattenedBuckets: FlattenedBucket[];
|
||||
maxChartHeight?: number;
|
||||
minChartHeight?: number;
|
||||
onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void;
|
||||
patternRollups: Record<string, PatternRollup>;
|
||||
patterns: string[];
|
||||
theme?: PartialTheme;
|
||||
valueFormatter: (value: number) => string;
|
||||
}
|
||||
|
||||
|
@ -86,16 +83,14 @@ export const getGroupByFieldsOnClick = (
|
|||
|
||||
const StorageTreemapComponent: React.FC<Props> = ({
|
||||
accessor,
|
||||
baseTheme,
|
||||
flattenedBuckets,
|
||||
maxChartHeight,
|
||||
minChartHeight = DEFAULT_MIN_CHART_HEIGHT,
|
||||
onIndexSelected,
|
||||
patternRollups,
|
||||
patterns,
|
||||
theme = {},
|
||||
valueFormatter,
|
||||
}: Props) => {
|
||||
const { theme, baseTheme, patterns } = useDataQualityContext();
|
||||
const fillColor = useMemo(
|
||||
() => theme?.background?.color ?? baseTheme.background.color,
|
||||
[theme?.background?.color, baseTheme.background.color]
|
||||
|
@ -159,7 +154,7 @@ const StorageTreemapComponent: React.FC<Props> = ({
|
|||
<Settings
|
||||
baseTheme={baseTheme}
|
||||
showLegend={false}
|
||||
theme={[treemapTheme, theme]}
|
||||
theme={[treemapTheme, theme || {}]}
|
||||
onElementClick={onElementClick}
|
||||
locale={i18n.getLocale()}
|
||||
/>
|
||||
|
|
|
@ -11,10 +11,15 @@ import React from 'react';
|
|||
import * as i18n from '../translations';
|
||||
|
||||
import { NoData } from '.';
|
||||
import { TestExternalProviders } from '../../../mock/test_providers/test_providers';
|
||||
|
||||
describe('NoData', () => {
|
||||
test('renders the expected "no data" message', () => {
|
||||
render(<NoData />);
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<NoData />
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByText(i18n.NO_DATA_LABEL)).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -19,21 +19,27 @@ interface Props {
|
|||
reason?: string;
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.eui.euiSizeM} 0;
|
||||
`;
|
||||
|
||||
const NoDataComponent: React.FC<Props> = ({ reason }) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow>
|
||||
<NoDataLabel color="subdued" data-test-subj="noDataLabel" size="xs">
|
||||
{i18n.NO_DATA_LABEL}
|
||||
</NoDataLabel>
|
||||
<StyledContainer>
|
||||
<NoDataLabel color="subdued" data-test-subj="noDataLabel" size="xs">
|
||||
{i18n.NO_DATA_LABEL}
|
||||
</NoDataLabel>
|
||||
|
||||
{reason != null && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<NoDataLabel color="subdued" data-test-subj="reasonLabel" size="xs">
|
||||
{reason}
|
||||
</NoDataLabel>
|
||||
</>
|
||||
)}
|
||||
{reason != null && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<NoDataLabel color="subdued" data-test-subj="reasonLabel" size="xs">
|
||||
{reason}
|
||||
</NoDataLabel>
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -5,28 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiScreenReaderOnly, EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import {
|
||||
CustomItemAction,
|
||||
EuiTableActionsColumnType,
|
||||
EuiTableFieldDataColumnType,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { omit } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { TestExternalProviders } from '../../mock/test_providers/test_providers';
|
||||
import { EMPTY_STAT } from '../../helpers';
|
||||
import {
|
||||
getDocsCountPercent,
|
||||
getResultIcon,
|
||||
getResultIconColor,
|
||||
getResultToolTip,
|
||||
getIncompatibleStatColor,
|
||||
getShowPagination,
|
||||
getSummaryTableColumns,
|
||||
getSummaryTableILMPhaseColumn,
|
||||
getSummaryTableSizeInBytesColumn,
|
||||
getToggleButtonId,
|
||||
IndexSummaryTableItem,
|
||||
} from './helpers';
|
||||
import { COLLAPSE, EXPAND, FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from './translations';
|
||||
import { CHECK_INDEX, VIEW_CHECK_DETAILS } from './translations';
|
||||
import { IndexSummaryTableItem } from '../pattern/types';
|
||||
import { getCheckState } from '../../stub/get_check_state';
|
||||
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
|
@ -37,48 +40,6 @@ const formatNumber = (value: number | undefined) =>
|
|||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getResultToolTip', () => {
|
||||
test('it shows a "this index has not been checked" tool tip when `incompatible` is undefined', () => {
|
||||
expect(getResultToolTip(undefined)).toEqual(THIS_INDEX_HAS_NOT_BEEN_CHECKED);
|
||||
});
|
||||
|
||||
test('it returns Passed when `incompatible` is zero', () => {
|
||||
expect(getResultToolTip(0)).toEqual(PASSED);
|
||||
});
|
||||
|
||||
test('it returns Failed when `incompatible` is NOT zero', () => {
|
||||
expect(getResultToolTip(1)).toEqual(FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResultIconColor', () => {
|
||||
test('it returns `ghost` when `incompatible` is undefined', () => {
|
||||
expect(getResultIconColor(undefined)).toEqual('ghost');
|
||||
});
|
||||
|
||||
test('it returns `success` when `incompatible` is zero', () => {
|
||||
expect(getResultIconColor(0)).toEqual('success');
|
||||
});
|
||||
|
||||
test('it returns `danger` when `incompatible` is NOT zero', () => {
|
||||
expect(getResultIconColor(1)).toEqual('danger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResultIcon', () => {
|
||||
test('it returns `cross` when `incompatible` is undefined', () => {
|
||||
expect(getResultIcon(undefined)).toEqual('cross');
|
||||
});
|
||||
|
||||
test('it returns `check` when `incompatible` is zero', () => {
|
||||
expect(getResultIcon(0)).toEqual('check');
|
||||
});
|
||||
|
||||
test('it returns `cross` when `incompatible` is NOT zero', () => {
|
||||
expect(getResultIcon(1)).toEqual('cross');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocsCountPercent', () => {
|
||||
test('it returns an empty string when `patternDocsCount` is zero', () => {
|
||||
expect(
|
||||
|
@ -156,22 +117,28 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
}).map((x) => omit('render', x));
|
||||
|
||||
expect(columns).toEqual([
|
||||
{
|
||||
align: 'right',
|
||||
isExpander: true,
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>{'Expand rows'}</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
width: '40px',
|
||||
name: 'Actions',
|
||||
align: 'center',
|
||||
width: '65px',
|
||||
actions: [
|
||||
{
|
||||
name: 'View check details',
|
||||
render: expect.any(Function),
|
||||
},
|
||||
{
|
||||
name: 'Check index',
|
||||
render: expect.any(Function),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'incompatible',
|
||||
|
@ -180,94 +147,141 @@ describe('helpers', () => {
|
|||
truncateText: false,
|
||||
width: '65px',
|
||||
},
|
||||
{ field: 'indexName', name: 'Index', sortable: true, truncateText: false, width: '300px' },
|
||||
{ field: 'docsCount', name: 'Docs', sortable: true, truncateText: false },
|
||||
{ field: 'indexName', name: 'Index', sortable: true, truncateText: false },
|
||||
{ field: 'docsCount', name: 'Docs', sortable: true, truncateText: false, width: '150px' },
|
||||
{
|
||||
field: 'incompatible',
|
||||
name: 'Incompatible fields',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '140px',
|
||||
},
|
||||
{
|
||||
field: 'ilmPhase',
|
||||
name: 'ILM Phase',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '92px',
|
||||
},
|
||||
{ field: 'sizeInBytes', name: 'Size', sortable: true, truncateText: false, width: '67px' },
|
||||
{
|
||||
field: 'checkedAt',
|
||||
name: 'Last check',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '120px',
|
||||
},
|
||||
{ field: 'ilmPhase', name: 'ILM Phase', sortable: true, truncateText: false },
|
||||
{ field: 'sizeInBytes', name: 'Size', sortable: true, truncateText: false },
|
||||
{ field: 'checkedAt', name: 'Last check', sortable: true, truncateText: false },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('expand rows render()', () => {
|
||||
test('it renders an Expand button when the row is NOT expanded', () => {
|
||||
describe('action columns render()', () => {
|
||||
test('it renders check index button', () => {
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
const checkNowRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[1] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
{expandRowsRender != null &&
|
||||
expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
{checkNowRender != null && checkNowRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(EXPAND)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(CHECK_INDEX)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it renders a Collapse button when the row is expanded', () => {
|
||||
const itemIdToExpandedRowMap: Record<string, React.ReactNode> = {
|
||||
[indexName]: () => null,
|
||||
};
|
||||
test('it invokes the `onCheckNowAction` with the index name when the check index button is clicked', () => {
|
||||
const onCheckNowAction = jest.fn();
|
||||
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap,
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction,
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
const checkNowRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[1] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
{expandRowsRender != null &&
|
||||
expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
{checkNowRender != null && checkNowRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(COLLAPSE)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it invokes the `toggleExpanded` with the index name when the button is clicked', () => {
|
||||
const toggleExpanded = jest.fn();
|
||||
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded,
|
||||
});
|
||||
const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
{expandRowsRender != null &&
|
||||
expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByLabelText(EXPAND);
|
||||
const button = screen.getByLabelText(CHECK_INDEX);
|
||||
userEvent.click(button);
|
||||
|
||||
expect(toggleExpanded).toBeCalledWith(indexName);
|
||||
expect(onCheckNowAction).toBeCalledWith(indexSummaryTableItem.indexName);
|
||||
});
|
||||
|
||||
test('it renders disabled check index with loading indicator when check state is loading', () => {
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName, { isChecking: true }),
|
||||
});
|
||||
|
||||
const checkNowRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[1] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
{checkNowRender != null && checkNowRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(CHECK_INDEX)).toBeDisabled();
|
||||
expect(screen.getByLabelText('Loading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it invokes the `onExpandAction` with the index name when the view check details button is clicked', () => {
|
||||
const onExpandAction = jest.fn();
|
||||
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction,
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
|
||||
const expandActionRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[0] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
{expandActionRender != null && expandActionRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByLabelText(VIEW_CHECK_DETAILS);
|
||||
userEvent.click(button);
|
||||
|
||||
expect(onExpandAction).toBeCalledWith(indexSummaryTableItem.indexName);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -281,45 +295,47 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const incompatibleRender = (
|
||||
columns[1] as EuiTableFieldDataColumnType<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{incompatibleRender != null &&
|
||||
incompatibleRender(incompatibleIsUndefined, incompatibleIsUndefined)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('incompatiblePlaceholder')).toHaveTextContent(EMPTY_STAT);
|
||||
});
|
||||
|
||||
test('it renders the expected icon when there are incompatible fields', () => {
|
||||
test('it renders Fail badge when there are incompatible fields', () => {
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const incompatibleRender = (
|
||||
columns[1] as EuiTableFieldDataColumnType<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'cross');
|
||||
expect(screen.getByText('Fail')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it renders the expected icon when there are zero fields', () => {
|
||||
|
@ -331,22 +347,23 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const incompatibleRender = (
|
||||
columns[1] as EuiTableFieldDataColumnType<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{incompatibleRender != null && incompatibleRender(zeroIncompatible, zeroIncompatible)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'check');
|
||||
expect(screen.getByText('Pass')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -355,19 +372,20 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const indexNameRender = (columns[2] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{indexNameRender != null &&
|
||||
indexNameRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('indexName')).toHaveTextContent(indexName);
|
||||
|
@ -379,18 +397,19 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const docsCountRender = (columns[3] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{docsCountRender != null && docsCountRender(hasIncompatible, hasIncompatible)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -414,19 +433,20 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const incompatibleRender = (
|
||||
columns[4] as EuiTableFieldDataColumnType<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('1');
|
||||
|
@ -436,20 +456,21 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const incompatibleRender = (
|
||||
columns[4] as EuiTableFieldDataColumnType<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{incompatibleRender != null &&
|
||||
incompatibleRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('--');
|
||||
|
@ -493,18 +514,19 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{ilmPhaseRender != null && ilmPhaseRender(hasIncompatible, hasIncompatible)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ilmPhase')).toHaveTextContent('hot');
|
||||
|
@ -519,18 +541,19 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument();
|
||||
|
@ -544,18 +567,19 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable: false,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument();
|
||||
|
@ -567,20 +591,21 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
|
||||
const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{sizeInBytesRender != null &&
|
||||
sizeInBytesRender(indexSummaryTableItem, indexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('sizeInBytes')).toHaveTextContent('98.6MB');
|
||||
|
@ -591,20 +616,21 @@ describe('helpers', () => {
|
|||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap: {},
|
||||
isILMAvailable,
|
||||
pattern: 'auditbeat-*',
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: getCheckState(indexName),
|
||||
});
|
||||
|
||||
const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
|
||||
.render;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<TestExternalProviders>
|
||||
{sizeInBytesRender != null &&
|
||||
sizeInBytesRender(testIndexSummaryTableItem, testIndexSummaryTableItem)}
|
||||
</TestProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sizeInBytes')).toBeNull();
|
||||
|
@ -640,4 +666,24 @@ describe('helpers', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncompatibleStatColor', () => {
|
||||
test('it returns the expected color when incompatible is greater than zero', () => {
|
||||
const incompatible = 123;
|
||||
|
||||
expect(getIncompatibleStatColor(incompatible)).toBe('#bd271e');
|
||||
});
|
||||
|
||||
test('it returns undefined when incompatible is zero', () => {
|
||||
const incompatible = 0;
|
||||
|
||||
expect(getIncompatibleStatColor(incompatible)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('it returns undefined when incompatible is undefined', () => {
|
||||
const incompatible = undefined;
|
||||
|
||||
expect(getIncompatibleStatColor(incompatible)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,69 +8,30 @@
|
|||
import {
|
||||
EuiBasicTableColumn,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
EuiProgress,
|
||||
EuiScreenReaderOnly,
|
||||
EuiStat,
|
||||
EuiToolTip,
|
||||
RIGHT_ALIGNMENT,
|
||||
CENTER_ALIGNMENT,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EMPTY_STAT, getIlmPhaseDescription, getIncompatibleStatColor } from '../../helpers';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EMPTY_STAT, getIlmPhaseDescription } from '../../helpers';
|
||||
import { INCOMPATIBLE_INDEX_TOOL_TIP } from '../stat_label/translations';
|
||||
import { INDEX_SIZE_TOOLTIP } from '../../translations';
|
||||
import * as i18n from './translations';
|
||||
import type { IlmPhase } from '../../types';
|
||||
|
||||
const STAT_TITLE_SIZE = 'xxs';
|
||||
|
||||
const EMPTY_DESCRIPTION = ' ';
|
||||
import { IndexSummaryTableItem } from '../pattern/types';
|
||||
import { UseIndicesCheckCheckState } from '../../use_indices_check/types';
|
||||
import { IndexResultBadge } from '../index_result_badge';
|
||||
import { getIndexResultToolTip } from '../index_result_badge/helpers';
|
||||
import { Stat } from '../pattern/pattern_summary/stats_rollup/stat';
|
||||
|
||||
const ProgressContainer = styled.div`
|
||||
width: 150px;
|
||||
`;
|
||||
|
||||
export interface IndexSummaryTableItem {
|
||||
docsCount: number;
|
||||
incompatible: number | undefined;
|
||||
indexName: string;
|
||||
ilmPhase: IlmPhase | undefined;
|
||||
pattern: string;
|
||||
patternDocsCount: number;
|
||||
sizeInBytes: number | undefined;
|
||||
checkedAt: number | undefined;
|
||||
}
|
||||
|
||||
export const getResultToolTip = (incompatible: number | undefined): string => {
|
||||
if (incompatible == null) {
|
||||
return i18n.THIS_INDEX_HAS_NOT_BEEN_CHECKED;
|
||||
} else if (incompatible === 0) {
|
||||
return i18n.PASSED;
|
||||
} else {
|
||||
return i18n.FAILED;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResultIconColor = (
|
||||
incompatible: number | undefined
|
||||
): 'success' | 'danger' | 'ghost' => {
|
||||
if (incompatible == null) {
|
||||
return 'ghost';
|
||||
} else if (incompatible === 0) {
|
||||
return 'success';
|
||||
} else {
|
||||
return 'danger';
|
||||
}
|
||||
};
|
||||
|
||||
export const getResultIcon = (incompatible: number | undefined): 'check' | 'cross' =>
|
||||
incompatible === 0 ? 'check' : 'cross';
|
||||
|
||||
export const getDocsCountPercent = ({
|
||||
docsCount,
|
||||
locales,
|
||||
|
@ -108,14 +69,17 @@ export const getSummaryTableILMPhaseColumn = (
|
|||
name: i18n.ILM_PHASE,
|
||||
render: (_, { ilmPhase }) =>
|
||||
ilmPhase != null ? (
|
||||
<EuiToolTip content={getIlmPhaseDescription(ilmPhase)}>
|
||||
<EuiBadge color="hollow" data-test-subj="ilmPhase">
|
||||
{ilmPhase}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
<Stat
|
||||
badgeText={ilmPhase}
|
||||
tooltipText={getIlmPhaseDescription(ilmPhase)}
|
||||
badgeProps={{
|
||||
'data-test-subj': 'ilmPhase',
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '92px',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
@ -140,61 +104,76 @@ export const getSummaryTableSizeInBytesColumn = ({
|
|||
) : null,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '67px',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
export const getIncompatibleStatColor = (incompatible: number | undefined): string | undefined =>
|
||||
incompatible != null && incompatible > 0 ? euiThemeVars.euiColorDanger : undefined;
|
||||
|
||||
export const getSummaryTableColumns = ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap,
|
||||
isILMAvailable,
|
||||
pattern,
|
||||
toggleExpanded,
|
||||
onExpandAction,
|
||||
onCheckNowAction,
|
||||
checkState,
|
||||
}: {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
itemIdToExpandedRowMap: Record<string, React.ReactNode>;
|
||||
isILMAvailable: boolean;
|
||||
pattern: string;
|
||||
toggleExpanded: (indexName: string) => void;
|
||||
onExpandAction: (indexName: string) => void;
|
||||
onCheckNowAction: (indexName: string) => void;
|
||||
checkState: UseIndicesCheckCheckState;
|
||||
}): Array<EuiBasicTableColumn<IndexSummaryTableItem>> => [
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
isExpander: true,
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>{i18n.EXPAND_ROWS}</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
render: ({ indexName }: IndexSummaryTableItem) => (
|
||||
<EuiButtonIcon
|
||||
aria-label={itemIdToExpandedRowMap[indexName] ? i18n.COLLAPSE : i18n.EXPAND}
|
||||
data-toggle-button-id={getToggleButtonId({
|
||||
indexName,
|
||||
isExpanded: itemIdToExpandedRowMap[indexName] != null,
|
||||
pattern,
|
||||
})}
|
||||
onClick={() => toggleExpanded(indexName)}
|
||||
iconType={itemIdToExpandedRowMap[indexName] ? 'arrowDown' : 'arrowRight'}
|
||||
/>
|
||||
),
|
||||
width: '40px',
|
||||
name: i18n.ACTIONS,
|
||||
align: CENTER_ALIGNMENT,
|
||||
width: '65px',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.VIEW_CHECK_DETAILS,
|
||||
render: (item) => {
|
||||
return (
|
||||
<EuiToolTip content={i18n.VIEW_CHECK_DETAILS}>
|
||||
<EuiButtonIcon
|
||||
iconType="expand"
|
||||
aria-label={i18n.VIEW_CHECK_DETAILS}
|
||||
onClick={() => onExpandAction(item.indexName)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.CHECK_INDEX,
|
||||
render: (item) => {
|
||||
const isChecking = checkState[item.indexName]?.isChecking ?? false;
|
||||
return (
|
||||
<EuiToolTip content={i18n.CHECK_INDEX}>
|
||||
<EuiButtonIcon
|
||||
iconType="refresh"
|
||||
aria-label={i18n.CHECK_INDEX}
|
||||
isLoading={isChecking}
|
||||
onClick={() => onCheckNowAction(item.indexName)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'incompatible',
|
||||
name: i18n.RESULT,
|
||||
render: (_, { incompatible }) =>
|
||||
incompatible != null ? (
|
||||
<EuiToolTip content={getResultToolTip(incompatible)}>
|
||||
<EuiIcon
|
||||
data-test-subj="resultIcon"
|
||||
color={getResultIconColor(incompatible)}
|
||||
type={getResultIcon(incompatible)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<IndexResultBadge incompatible={incompatible} data-test-subj="resultBadge" />
|
||||
) : (
|
||||
<EuiToolTip content={getResultToolTip(incompatible)}>
|
||||
<EuiToolTip content={getIndexResultToolTip(incompatible)}>
|
||||
<span data-test-subj="incompatiblePlaceholder">{EMPTY_STAT}</span>
|
||||
</EuiToolTip>
|
||||
),
|
||||
|
@ -214,7 +193,6 @@ export const getSummaryTableColumns = ({
|
|||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '300px',
|
||||
},
|
||||
{
|
||||
field: 'docsCount',
|
||||
|
@ -233,24 +211,25 @@ export const getSummaryTableColumns = ({
|
|||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
field: 'incompatible',
|
||||
name: i18n.INCOMPATIBLE_FIELDS,
|
||||
render: (_, { incompatible, indexName }) => (
|
||||
<EuiToolTip content={INCOMPATIBLE_INDEX_TOOL_TIP(indexName)}>
|
||||
<EuiStat
|
||||
<EuiToolTip content={INCOMPATIBLE_INDEX_TOOL_TIP}>
|
||||
<EuiText
|
||||
size="xs"
|
||||
data-test-subj="incompatibleStat"
|
||||
description={EMPTY_DESCRIPTION}
|
||||
reverse
|
||||
title={incompatible ?? EMPTY_STAT}
|
||||
titleColor={getIncompatibleStatColor(incompatible)}
|
||||
titleSize={STAT_TITLE_SIZE}
|
||||
/>
|
||||
color={getIncompatibleStatColor(incompatible)}
|
||||
>
|
||||
{incompatible ?? EMPTY_STAT}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '140px',
|
||||
},
|
||||
...getSummaryTableILMPhaseColumn(isILMAvailable),
|
||||
...getSummaryTableSizeInBytesColumn({ isILMAvailable, formatBytes }),
|
||||
|
@ -264,6 +243,7 @@ export const getSummaryTableColumns = ({
|
|||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '120px',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -14,7 +14,10 @@ import { getSummaryTableColumns } from './helpers';
|
|||
import { mockIlmExplain } from '../../mock/ilm_explain/mock_ilm_explain';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { mockStats } from '../../mock/stats/mock_stats';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../mock/test_providers/test_providers';
|
||||
import { getSummaryTableItems } from '../pattern/helpers';
|
||||
import { SortConfig } from '../../types';
|
||||
import { Props, SummaryTable } from '.';
|
||||
|
@ -58,10 +61,7 @@ const items = getSummaryTableItems({
|
|||
});
|
||||
|
||||
const defaultProps: Props = {
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getTableColumns: getSummaryTableColumns,
|
||||
itemIdToExpandedRowMap: {},
|
||||
items,
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
|
@ -70,7 +70,27 @@ const defaultProps: Props = {
|
|||
setPageSize: jest.fn(),
|
||||
setSorting: jest.fn(),
|
||||
sorting: defaultSort,
|
||||
toggleExpanded: jest.fn(),
|
||||
onCheckNowAction: jest.fn(),
|
||||
onExpandAction: jest.fn(),
|
||||
checkState: Object.fromEntries(
|
||||
indexNames.map((indexName) => [
|
||||
indexName,
|
||||
{
|
||||
isChecking: false,
|
||||
isLoadingMappings: false,
|
||||
isLoadingUnallowedValues: false,
|
||||
indexes: null,
|
||||
mappingsProperties: null,
|
||||
unallowedValues: null,
|
||||
genericError: null,
|
||||
mappingsError: null,
|
||||
unallowedValuesError: null,
|
||||
partitionedFieldMetadata: null,
|
||||
isCheckComplete: false,
|
||||
searchResults: null,
|
||||
},
|
||||
])
|
||||
),
|
||||
};
|
||||
|
||||
describe('SummaryTable', () => {
|
||||
|
@ -78,9 +98,11 @@ describe('SummaryTable', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<SummaryTable {...defaultProps} />
|
||||
</TestProviders>
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders dataQualityContextProps={{ formatBytes, formatNumber }}>
|
||||
<SummaryTable {...defaultProps} />
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,31 +9,31 @@ import type { CriteriaWithPagination, EuiBasicTableColumn, Pagination } from '@e
|
|||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { IndexSummaryTableItem } from './helpers';
|
||||
import { getShowPagination } from './helpers';
|
||||
import { defaultSort, MIN_PAGE_SIZE } from '../pattern/helpers';
|
||||
import { SortConfig } from '../../types';
|
||||
import { useDataQualityContext } from '../data_quality_context';
|
||||
import { IndexSummaryTableItem } from '../pattern/types';
|
||||
import { UseIndicesCheckCheckState } from '../../use_indices_check/types';
|
||||
|
||||
export interface Props {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
getTableColumns: ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap,
|
||||
checkState,
|
||||
isILMAvailable,
|
||||
pattern,
|
||||
toggleExpanded,
|
||||
onExpandAction,
|
||||
onCheckNowAction,
|
||||
}: {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
itemIdToExpandedRowMap: Record<string, React.ReactNode>;
|
||||
checkState: UseIndicesCheckCheckState;
|
||||
isILMAvailable: boolean;
|
||||
pattern: string;
|
||||
toggleExpanded: (indexName: string) => void;
|
||||
onExpandAction: (indexName: string) => void;
|
||||
onCheckNowAction: (indexName: string) => void;
|
||||
}) => Array<EuiBasicTableColumn<IndexSummaryTableItem>>;
|
||||
itemIdToExpandedRowMap: Record<string, React.ReactNode>;
|
||||
items: IndexSummaryTableItem[];
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
|
@ -42,14 +42,13 @@ export interface Props {
|
|||
setPageSize: (pageSize: number) => void;
|
||||
setSorting: (sortConfig: SortConfig) => void;
|
||||
sorting: SortConfig;
|
||||
toggleExpanded: (indexName: string) => void;
|
||||
onExpandAction: (indexName: string) => void;
|
||||
onCheckNowAction: (indexName: string) => void;
|
||||
checkState: UseIndicesCheckCheckState;
|
||||
}
|
||||
|
||||
const SummaryTableComponent: React.FC<Props> = ({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getTableColumns,
|
||||
itemIdToExpandedRowMap,
|
||||
items,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
|
@ -58,27 +57,31 @@ const SummaryTableComponent: React.FC<Props> = ({
|
|||
setPageSize,
|
||||
setSorting,
|
||||
sorting,
|
||||
toggleExpanded,
|
||||
onExpandAction,
|
||||
onCheckNowAction,
|
||||
checkState,
|
||||
}) => {
|
||||
const { isILMAvailable } = useDataQualityContext();
|
||||
const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext();
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
itemIdToExpandedRowMap,
|
||||
checkState,
|
||||
isILMAvailable,
|
||||
pattern,
|
||||
toggleExpanded,
|
||||
onExpandAction,
|
||||
onCheckNowAction,
|
||||
}),
|
||||
[
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
getTableColumns,
|
||||
checkState,
|
||||
isILMAvailable,
|
||||
itemIdToExpandedRowMap,
|
||||
onCheckNowAction,
|
||||
onExpandAction,
|
||||
pattern,
|
||||
toggleExpanded,
|
||||
]
|
||||
);
|
||||
const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []);
|
||||
|
@ -109,7 +112,6 @@ const SummaryTableComponent: React.FC<Props> = ({
|
|||
columns={columns}
|
||||
data-test-subj="summaryTable"
|
||||
itemId={getItemId}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
items={items}
|
||||
onChange={onChange}
|
||||
pagination={
|
||||
|
|
|
@ -21,10 +21,10 @@ export const DOCS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EXPAND = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandLabel',
|
||||
export const VIEW_CHECK_DETAILS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.viewCheckDetailsLabel',
|
||||
{
|
||||
defaultMessage: 'Expand',
|
||||
defaultMessage: 'View check details',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -124,3 +124,17 @@ export const THIS_INDEX_HAS_NOT_BEEN_CHECKED = i18n.translate(
|
|||
defaultMessage: 'This index has not been checked',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTIONS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.actionsColumn',
|
||||
{
|
||||
defaultMessage: 'Actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHECK_INDEX = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.summaryTable.checkIndexButton',
|
||||
{
|
||||
defaultMessage: 'Check index',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -26,10 +26,10 @@ const AllTabComponent: React.FC<Props> = ({ indexName, partitionedFieldMetadata
|
|||
const title = useMemo(() => <EmptyPromptTitle title={i18n.ALL_EMPTY_TITLE} />, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-test-subj="allTabContent">
|
||||
{partitionedFieldMetadata.all.length > 0 ? (
|
||||
<>
|
||||
<EuiCallOut size="s" title={i18n.ALL_CALLOUT_TITLE(partitionedFieldMetadata.all.length)}>
|
||||
<EuiCallOut size="s">
|
||||
<p>{i18n.ALL_CALLOUT(EcsVersion)}</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
|
@ -48,7 +48,7 @@ const AllTabComponent: React.FC<Props> = ({ indexName, partitionedFieldMetadata
|
|||
titleSize="s"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue