mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][DQD] Add historical results tour guide (#196127)](https://github.com/elastic/kibana/pull/196127) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Karen Grigoryan","email":"karen.grigoryan@elastic.co"},"sourceCommit":{"committedDate":"2024-10-15T23:18:50Z","message":"[Security Solution][DQD] Add historical results tour guide (#196127)\n\naddresses #195971\r\n\r\nThis PR adds missing new historical results feature tour guide.\r\n\r\n## Tour guide features:\r\n- ability to maintain visual presence while collapsing accordions in\r\nlist-view\r\n- move from list-view to flyout view and back\r\n- seamlessly integrates with existing opening flyout and history tab\r\nfunctionality\r\n\r\n## PR decisions with explanation:\r\n- data-tour-element has been introduced on select elements (like first\r\nactions of each first row) to avoid polluting every single element with\r\ndata-test-subj. This way it's imho specific and semantically more clear\r\nwhat the elements are for.\r\n- early on I tried to control the anchoring with refs but some eui\r\nelements don't allow passing refs like EuiTab, so instead a more simpler\r\nand straightforward approach with dom selectors has been chosen\r\n- localStorage key name has been picked in accordance with other\r\ninstances of usage\r\n`securitySolution.dataQualityDashboard.historicalResultsTour.v8.16.isActive`\r\nthe name includes the full domain + the version when it's introduced.\r\nAnd since this tour step is a single step there is no need to stringify\r\nan object with `isTourActive` in and it's much simpler to just bake the\r\nactivity state into the name and make the value just a boolean.\r\n\r\n## UI Demo\r\n\r\n### Anchor reposition demo (listview + flyout)\r\n\r\nhttps://github.com/user-attachments/assets/0f961c51-0e36-48ca-aab4-bef3b0d1269e\r\n\r\n### List view tour guide try it + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/ca1f5fda-ee02-4a48-827c-91df757a8ddf\r\n\r\n### FlyOut Try It + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d0801ac3-1ed1-4e64-9d6b-3140b8402bdf\r\n\r\n### Manual history tab selection path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/34dbb447-2fd6-4dc0-a4f5-682c9c65cc8b\r\n\r\n### Manual open history view path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/945dd042-fc12-476e-8d23-f48c9ded9f65\r\n\r\n### Dismiss list view tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d20d1416-827f-46f2-9161-a3c0a8cbd932\r\n\r\n### Dismiss FlyOut tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/8f085f59-20a9-49f0-b5b3-959c4719f5cb\r\n\r\n### Serverless empty pattern handling + reposition demo\r\n\r\nhttps://github.com/user-attachments/assets/4af5939e-663c-4439-a3fc-deff2d4de7e4","sha":"c448593d546f6200b0d2d35bce043bef521f41a6","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","Team:Threat Hunting","release_note:feature","Team:Threat Hunting:Explore","backport:prev-minor"],"title":"[Security Solution][DQD] Add historical results tour guide","number":196127,"url":"https://github.com/elastic/kibana/pull/196127","mergeCommit":{"message":"[Security Solution][DQD] Add historical results tour guide (#196127)\n\naddresses #195971\r\n\r\nThis PR adds missing new historical results feature tour guide.\r\n\r\n## Tour guide features:\r\n- ability to maintain visual presence while collapsing accordions in\r\nlist-view\r\n- move from list-view to flyout view and back\r\n- seamlessly integrates with existing opening flyout and history tab\r\nfunctionality\r\n\r\n## PR decisions with explanation:\r\n- data-tour-element has been introduced on select elements (like first\r\nactions of each first row) to avoid polluting every single element with\r\ndata-test-subj. This way it's imho specific and semantically more clear\r\nwhat the elements are for.\r\n- early on I tried to control the anchoring with refs but some eui\r\nelements don't allow passing refs like EuiTab, so instead a more simpler\r\nand straightforward approach with dom selectors has been chosen\r\n- localStorage key name has been picked in accordance with other\r\ninstances of usage\r\n`securitySolution.dataQualityDashboard.historicalResultsTour.v8.16.isActive`\r\nthe name includes the full domain + the version when it's introduced.\r\nAnd since this tour step is a single step there is no need to stringify\r\nan object with `isTourActive` in and it's much simpler to just bake the\r\nactivity state into the name and make the value just a boolean.\r\n\r\n## UI Demo\r\n\r\n### Anchor reposition demo (listview + flyout)\r\n\r\nhttps://github.com/user-attachments/assets/0f961c51-0e36-48ca-aab4-bef3b0d1269e\r\n\r\n### List view tour guide try it + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/ca1f5fda-ee02-4a48-827c-91df757a8ddf\r\n\r\n### FlyOut Try It + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d0801ac3-1ed1-4e64-9d6b-3140b8402bdf\r\n\r\n### Manual history tab selection path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/34dbb447-2fd6-4dc0-a4f5-682c9c65cc8b\r\n\r\n### Manual open history view path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/945dd042-fc12-476e-8d23-f48c9ded9f65\r\n\r\n### Dismiss list view tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d20d1416-827f-46f2-9161-a3c0a8cbd932\r\n\r\n### Dismiss FlyOut tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/8f085f59-20a9-49f0-b5b3-959c4719f5cb\r\n\r\n### Serverless empty pattern handling + reposition demo\r\n\r\nhttps://github.com/user-attachments/assets/4af5939e-663c-4439-a3fc-deff2d4de7e4","sha":"c448593d546f6200b0d2d35bce043bef521f41a6"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196127","number":196127,"mergeCommit":{"message":"[Security Solution][DQD] Add historical results tour guide (#196127)\n\naddresses #195971\r\n\r\nThis PR adds missing new historical results feature tour guide.\r\n\r\n## Tour guide features:\r\n- ability to maintain visual presence while collapsing accordions in\r\nlist-view\r\n- move from list-view to flyout view and back\r\n- seamlessly integrates with existing opening flyout and history tab\r\nfunctionality\r\n\r\n## PR decisions with explanation:\r\n- data-tour-element has been introduced on select elements (like first\r\nactions of each first row) to avoid polluting every single element with\r\ndata-test-subj. This way it's imho specific and semantically more clear\r\nwhat the elements are for.\r\n- early on I tried to control the anchoring with refs but some eui\r\nelements don't allow passing refs like EuiTab, so instead a more simpler\r\nand straightforward approach with dom selectors has been chosen\r\n- localStorage key name has been picked in accordance with other\r\ninstances of usage\r\n`securitySolution.dataQualityDashboard.historicalResultsTour.v8.16.isActive`\r\nthe name includes the full domain + the version when it's introduced.\r\nAnd since this tour step is a single step there is no need to stringify\r\nan object with `isTourActive` in and it's much simpler to just bake the\r\nactivity state into the name and make the value just a boolean.\r\n\r\n## UI Demo\r\n\r\n### Anchor reposition demo (listview + flyout)\r\n\r\nhttps://github.com/user-attachments/assets/0f961c51-0e36-48ca-aab4-bef3b0d1269e\r\n\r\n### List view tour guide try it + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/ca1f5fda-ee02-4a48-827c-91df757a8ddf\r\n\r\n### FlyOut Try It + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d0801ac3-1ed1-4e64-9d6b-3140b8402bdf\r\n\r\n### Manual history tab selection path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/34dbb447-2fd6-4dc0-a4f5-682c9c65cc8b\r\n\r\n### Manual open history view path + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/945dd042-fc12-476e-8d23-f48c9ded9f65\r\n\r\n### Dismiss list view tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/d20d1416-827f-46f2-9161-a3c0a8cbd932\r\n\r\n### Dismiss FlyOut tour guide + reload demo\r\n\r\nhttps://github.com/user-attachments/assets/8f085f59-20a9-49f0-b5b3-959c4719f5cb\r\n\r\n### Serverless empty pattern handling + reposition demo\r\n\r\nhttps://github.com/user-attachments/assets/4af5939e-663c-4439-a3fc-deff2d4de7e4","sha":"c448593d546f6200b0d2d35bce043bef521f41a6"}}]}] BACKPORT--> Co-authored-by: Karen Grigoryan <karen.grigoryan@elastic.co>
This commit is contained in:
parent
b8fcdcc933
commit
1549d38d02
16 changed files with 1304 additions and 26 deletions
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY =
|
||||
'securitySolution.dataQualityDashboard.historicalResultsTour.v8.16.isDismissed';
|
|
@ -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 { useCallback } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
|
||||
import { HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY } from '../../constants';
|
||||
|
||||
export const useIsHistoricalResultsTourActive = () => {
|
||||
const [isTourDismissed, setIsTourDismissed] = useLocalStorage<boolean>(
|
||||
HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
const isTourActive = !isTourDismissed;
|
||||
const setIsTourActive = useCallback(
|
||||
(active: boolean) => {
|
||||
setIsTourDismissed(!active);
|
||||
},
|
||||
[setIsTourDismissed]
|
||||
);
|
||||
|
||||
return [isTourActive, setIsTourActive] as const;
|
||||
};
|
|
@ -6,12 +6,15 @@
|
|||
*/
|
||||
|
||||
import numeral from '@elastic/numeral';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { EMPTY_STAT } from '../../constants';
|
||||
import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup';
|
||||
import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import {
|
||||
auditbeatWithAllResults,
|
||||
emptyAuditbeatPatternRollup,
|
||||
} from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup';
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
|
@ -19,6 +22,8 @@ import {
|
|||
} from '../../mock/test_providers/test_providers';
|
||||
import { PatternRollup } from '../../types';
|
||||
import { Props, IndicesDetails } from '.';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY } from './constants';
|
||||
|
||||
const defaultBytesFormat = '0,0.[0]b';
|
||||
const formatBytes = (value: number | undefined) =>
|
||||
|
@ -29,15 +34,22 @@ const formatNumber = (value: number | undefined) =>
|
|||
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
|
||||
|
||||
const ilmPhases = ['hot', 'warm', 'unmanaged'];
|
||||
const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*'];
|
||||
const patterns = [
|
||||
'test-empty-pattern-*',
|
||||
'.alerts-security.alerts-default',
|
||||
'auditbeat-*',
|
||||
'packetbeat-*',
|
||||
];
|
||||
|
||||
const patternRollups: Record<string, PatternRollup> = {
|
||||
'test-empty-pattern-*': { ...emptyAuditbeatPatternRollup, pattern: 'test-empty-pattern-*' },
|
||||
'.alerts-security.alerts-default': alertIndexWithAllResults,
|
||||
'auditbeat-*': auditbeatWithAllResults,
|
||||
'packetbeat-*': packetbeatNoResults,
|
||||
};
|
||||
|
||||
const patternIndexNames: Record<string, string[]> = {
|
||||
'test-empty-pattern-*': [],
|
||||
'auditbeat-*': [
|
||||
'.ds-auditbeat-8.6.1-2023.02.07-000001',
|
||||
'auditbeat-custom-empty-index-1',
|
||||
|
@ -58,6 +70,7 @@ const defaultProps: Props = {
|
|||
describe('IndicesDetails', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.removeItem(HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY);
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
|
@ -74,10 +87,64 @@ describe('IndicesDetails', () => {
|
|||
});
|
||||
|
||||
describe('rendering patterns', () => {
|
||||
patterns.forEach((pattern) => {
|
||||
test(`it renders the ${pattern} pattern`, () => {
|
||||
expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument();
|
||||
test.each(patterns)('it renders the %s pattern', (pattern) => {
|
||||
expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tour', () => {
|
||||
test('it renders the tour wrapping view history button of first row of first non-empty pattern', async () => {
|
||||
const wrapper = await screen.findByTestId('historicalResultsTour');
|
||||
const button = within(wrapper).getByRole('button', { name: 'View history' });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('data-tour-element', patterns[1]);
|
||||
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when the tour is dismissed', () => {
|
||||
test('it hides the tour and persists in localStorage', async () => {
|
||||
const wrapper = await screen.findByRole('dialog', {
|
||||
name: 'Introducing data quality history',
|
||||
});
|
||||
|
||||
const button = within(wrapper).getByRole('button', { name: 'Close' });
|
||||
|
||||
await userEvent.click(button);
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('historicalResultsTour')).toBeNull());
|
||||
|
||||
expect(localStorage.getItem(HISTORICAL_RESULTS_TOUR_IS_DISMISSED_STORAGE_KEY)).toEqual(
|
||||
'true'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the first pattern is toggled', () => {
|
||||
test('it renders the tour wrapping view history button of first row of second non-empty pattern', async () => {
|
||||
const firstNonEmptyPatternAccordionWrapper = await screen.findByTestId(
|
||||
`${patterns[1]}PatternPanel`
|
||||
);
|
||||
const accordionToggle = within(firstNonEmptyPatternAccordionWrapper).getByRole('button', {
|
||||
name: /Pass/,
|
||||
});
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
const secondPatternAccordionWrapper = screen.getByTestId(`${patterns[2]}PatternPanel`);
|
||||
const historicalResultsWrapper = await within(secondPatternAccordionWrapper).findByTestId(
|
||||
'historicalResultsTour'
|
||||
);
|
||||
const button = within(historicalResultsWrapper).getByRole('button', {
|
||||
name: 'View history',
|
||||
});
|
||||
expect(button).toHaveAttribute('data-tour-element', patterns[2]);
|
||||
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useResultsRollupContext } from '../../contexts/results_rollup_context';
|
||||
import { Pattern } from './pattern';
|
||||
import { SelectedIndex } from '../../types';
|
||||
import { useDataQualityContext } from '../../data_quality_context';
|
||||
import { useIsHistoricalResultsTourActive } from './hooks/use_is_historical_results_tour_active';
|
||||
|
||||
const StyledPatternWrapperFlexItem = styled(EuiFlexItem)`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSize};
|
||||
|
@ -34,6 +35,41 @@ const IndicesDetailsComponent: React.FC<Props> = ({
|
|||
const { patternRollups, patternIndexNames } = useResultsRollupContext();
|
||||
const { patterns } = useDataQualityContext();
|
||||
|
||||
const [isTourActive, setIsTourActive] = useIsHistoricalResultsTourActive();
|
||||
|
||||
const handleDismissTour = useCallback(() => {
|
||||
setIsTourActive(false);
|
||||
}, [setIsTourActive]);
|
||||
|
||||
const [openPatterns, setOpenPatterns] = useState<
|
||||
Array<{ name: string; isOpen: boolean; isEmpty: boolean }>
|
||||
>(() => {
|
||||
return patterns.map((pattern) => ({ name: pattern, isOpen: true, isEmpty: false }));
|
||||
});
|
||||
|
||||
const handleAccordionToggle = useCallback(
|
||||
(patternName: string, isOpen: boolean, isEmpty: boolean) => {
|
||||
setOpenPatterns((prevOpenPatterns) => {
|
||||
return prevOpenPatterns.map((p) =>
|
||||
p.name === patternName ? { ...p, isOpen, isEmpty } : p
|
||||
);
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const firstOpenNonEmptyPattern = openPatterns.find((pattern) => {
|
||||
return pattern.isOpen && !pattern.isEmpty;
|
||||
})?.name;
|
||||
|
||||
const [openPatternsUpdatedAt, setOpenPatternsUpdatedAt] = useState<number>(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (firstOpenNonEmptyPattern) {
|
||||
setOpenPatternsUpdatedAt(Date.now());
|
||||
}
|
||||
}, [openPatterns, firstOpenNonEmptyPattern]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="indicesDetails">
|
||||
{patterns.map((pattern) => (
|
||||
|
@ -44,6 +80,16 @@ const IndicesDetailsComponent: React.FC<Props> = ({
|
|||
patternRollup={patternRollups[pattern]}
|
||||
chartSelectedIndex={chartSelectedIndex}
|
||||
setChartSelectedIndex={setChartSelectedIndex}
|
||||
isTourActive={isTourActive}
|
||||
isFirstOpenNonEmptyPattern={pattern === firstOpenNonEmptyPattern}
|
||||
onAccordionToggle={handleAccordionToggle}
|
||||
onDismissTour={handleDismissTour}
|
||||
// TODO: remove this hack when EUI popover is fixed
|
||||
// https://github.com/elastic/eui/issues/5226
|
||||
//
|
||||
// this information is used to force the tour guide popover to reposition
|
||||
// when surrounding accordions get toggled and affect the layout
|
||||
{...(pattern === firstOpenNonEmptyPattern && { openPatternsUpdatedAt })}
|
||||
/>
|
||||
</StyledPatternWrapperFlexItem>
|
||||
))}
|
||||
|
|
|
@ -9,3 +9,5 @@ export const MIN_PAGE_SIZE = 10;
|
|||
|
||||
export const HISTORY_TAB_ID = 'history';
|
||||
export const LATEST_CHECK_TAB_ID = 'latest_check';
|
||||
|
||||
export const HISTORICAL_RESULTS_TOUR_SELECTOR_KEY = 'data-tour-element';
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 { HISTORICAL_RESULTS_TOUR_SELECTOR_KEY } from '../constants';
|
||||
import { HistoricalResultsTour } from '.';
|
||||
import { INTRODUCING_DATA_QUALITY_HISTORY, VIEW_PAST_RESULTS } from './translations';
|
||||
|
||||
const anchorSelectorValue = 'test-anchor';
|
||||
|
||||
describe('HistoricalResultsTour', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('given no anchor element', () => {
|
||||
it('does not render the tour step', () => {
|
||||
render(
|
||||
<HistoricalResultsTour
|
||||
anchorSelectorValue={anchorSelectorValue}
|
||||
onTryIt={jest.fn()}
|
||||
isOpen={true}
|
||||
onDismissTour={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(INTRODUCING_DATA_QUALITY_HISTORY)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an anchor element', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
document.body.innerHTML = `<div ${HISTORICAL_RESULTS_TOUR_SELECTOR_KEY}="${anchorSelectorValue}"></div>`;
|
||||
});
|
||||
|
||||
describe('when isOpen is true', () => {
|
||||
const onTryIt = jest.fn();
|
||||
const onDismissTour = jest.fn();
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<HistoricalResultsTour
|
||||
anchorSelectorValue={anchorSelectorValue}
|
||||
onTryIt={onTryIt}
|
||||
isOpen={true}
|
||||
onDismissTour={onDismissTour}
|
||||
/>
|
||||
);
|
||||
});
|
||||
it('renders the tour step', async () => {
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: INTRODUCING_DATA_QUALITY_HISTORY })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(INTRODUCING_DATA_QUALITY_HISTORY)).toBeInTheDocument();
|
||||
expect(screen.getByText(VIEW_PAST_RESULTS)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Close/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Try It/i })).toBeInTheDocument();
|
||||
|
||||
const historicalResultsTour = screen.getByTestId('historicalResultsTour');
|
||||
expect(historicalResultsTour.querySelector('[data-tour-element]')).toHaveAttribute(
|
||||
'data-tour-element',
|
||||
anchorSelectorValue
|
||||
);
|
||||
});
|
||||
|
||||
describe('when the close button is clicked', () => {
|
||||
it('calls dismissTour', async () => {
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Close/i }));
|
||||
expect(onDismissTour).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the try it button is clicked', () => {
|
||||
it('calls onTryIt', async () => {
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Try It/i }));
|
||||
expect(onTryIt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isOpen is false', () => {
|
||||
it('does not render the tour step', async () => {
|
||||
render(
|
||||
<HistoricalResultsTour
|
||||
anchorSelectorValue={anchorSelectorValue}
|
||||
onTryIt={jest.fn()}
|
||||
isOpen={false}
|
||||
onDismissTour={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(INTRODUCING_DATA_QUALITY_HISTORY)).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { EuiButton, EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HISTORICAL_RESULTS_TOUR_SELECTOR_KEY } from '../constants';
|
||||
import { CLOSE, INTRODUCING_DATA_QUALITY_HISTORY, TRY_IT, VIEW_PAST_RESULTS } from './translations';
|
||||
|
||||
export interface Props {
|
||||
anchorSelectorValue: string;
|
||||
isOpen: boolean;
|
||||
onTryIt: () => void;
|
||||
onDismissTour: () => void;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const StyledText = styled(EuiText)`
|
||||
margin-block-start: -10px;
|
||||
`;
|
||||
|
||||
export const HistoricalResultsTour: FC<Props> = ({
|
||||
anchorSelectorValue,
|
||||
onTryIt,
|
||||
isOpen,
|
||||
onDismissTour,
|
||||
zIndex,
|
||||
}) => {
|
||||
const [anchorElement, setAnchorElement] = useState<HTMLElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector<HTMLElement>(
|
||||
`[${HISTORICAL_RESULTS_TOUR_SELECTOR_KEY}="${anchorSelectorValue}"]`
|
||||
);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAnchorElement(element);
|
||||
}, [anchorSelectorValue]);
|
||||
|
||||
if (!isOpen || !anchorElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTourStep
|
||||
content={
|
||||
<StyledText size="s">
|
||||
<p>{VIEW_PAST_RESULTS}</p>
|
||||
</StyledText>
|
||||
}
|
||||
data-test-subj="historicalResultsTour"
|
||||
isStepOpen={isOpen}
|
||||
minWidth={283}
|
||||
onFinish={onDismissTour}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={INTRODUCING_DATA_QUALITY_HISTORY}
|
||||
anchorPosition="rightUp"
|
||||
repositionOnScroll
|
||||
anchor={anchorElement}
|
||||
zIndex={zIndex}
|
||||
footerAction={[
|
||||
<EuiButtonEmpty size="xs" color="text" onClick={onDismissTour}>
|
||||
{CLOSE}
|
||||
</EuiButtonEmpty>,
|
||||
<EuiButton color="success" size="s" onClick={onTryIt}>
|
||||
{TRY_IT}
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CLOSE = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.close', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
export const TRY_IT = i18n.translate('securitySolutionPackages.ecsDataQualityDashboard.tryIt', {
|
||||
defaultMessage: 'Try it',
|
||||
});
|
||||
|
||||
export const INTRODUCING_DATA_QUALITY_HISTORY = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.introducingDataQualityHistory',
|
||||
{
|
||||
defaultMessage: 'Introducing data quality history',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_PAST_RESULTS = i18n.translate(
|
||||
'securitySolutionPackages.ecsDataQualityDashboard.viewPastResults',
|
||||
{
|
||||
defaultMessage: 'View past results',
|
||||
}
|
||||
);
|
|
@ -6,19 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
TestDataQualityProviders,
|
||||
TestExternalProviders,
|
||||
} from '../../../mock/test_providers/test_providers';
|
||||
import { Pattern } from '.';
|
||||
import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import {
|
||||
auditbeatWithAllResults,
|
||||
emptyAuditbeatPatternRollup,
|
||||
} from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup';
|
||||
import { useIlmExplain } from './hooks/use_ilm_explain';
|
||||
import { useStats } from './hooks/use_stats';
|
||||
import { ERROR_LOADING_METADATA_TITLE, LOADING_STATS } from './translations';
|
||||
import { useHistoricalResults } from './hooks/use_historical_results';
|
||||
import { getHistoricalResultStub } from '../../../stub/get_historical_result_stub';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const pattern = 'auditbeat-*';
|
||||
|
||||
|
@ -81,6 +85,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -95,6 +103,157 @@ describe('pattern', () => {
|
|||
expect(screen.getByTestId('summaryTable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('onAccordionToggle', () => {
|
||||
describe('by default', () => {
|
||||
describe('when no summary table items are available', () => {
|
||||
it('invokes the onAccordionToggle function with the pattern name, isOpen as true and isEmpty as true', async () => {
|
||||
const onAccordionToggle = jest.fn();
|
||||
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={emptyAuditbeatPatternRollup}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={[]}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={onAccordionToggle}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const accordionToggle = await screen.findByRole('button', {
|
||||
name: 'auditbeat-* Incompatible fields 0 Indices checked 0 Indices 0 Size 0B Docs 0',
|
||||
});
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(1);
|
||||
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(2);
|
||||
expect(onAccordionToggle).toHaveBeenCalledWith(pattern, true, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when summary table items are available', () => {
|
||||
it('invokes the onAccordionToggle function with the pattern name, isOpen as true and isEmpty as false', async () => {
|
||||
const onAccordionToggle = jest.fn();
|
||||
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={onAccordionToggle}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const accordionToggle = screen.getByRole('button', {
|
||||
name: 'Fail auditbeat-* hot (1) unmanaged (2) Incompatible fields 4 Indices checked 3 Indices 3 Size 17.9MB Docs 19,127',
|
||||
});
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(1);
|
||||
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(2);
|
||||
expect(onAccordionToggle).toHaveBeenCalledWith(pattern, true, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the accordion is toggled', () => {
|
||||
it('calls the onAccordionToggle function with current open state and current empty state', async () => {
|
||||
const onAccordionToggle = jest.fn();
|
||||
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={onAccordionToggle}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const accordionToggle = screen.getByRole('button', {
|
||||
name: 'Fail auditbeat-* hot (1) unmanaged (2) Incompatible fields 4 Indices checked 3 Indices 3 Size 17.9MB Docs 19,127',
|
||||
});
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(1);
|
||||
expect(onAccordionToggle).toHaveBeenCalledWith(pattern, true, false);
|
||||
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(2);
|
||||
expect(onAccordionToggle).toHaveBeenLastCalledWith(pattern, false, false);
|
||||
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
expect(onAccordionToggle).toHaveBeenCalledTimes(3);
|
||||
expect(onAccordionToggle).toHaveBeenCalledWith(pattern, true, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote clusters callout', () => {
|
||||
describe('when the pattern includes a colon', () => {
|
||||
it('it renders the remote clusters callout', () => {
|
||||
|
@ -107,6 +266,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={undefined}
|
||||
pattern={'remote:*'}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -127,6 +290,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={undefined}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -155,6 +322,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -182,6 +353,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -215,6 +390,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -248,6 +427,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -292,6 +475,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -306,7 +493,7 @@ describe('pattern', () => {
|
|||
name: 'Check now',
|
||||
});
|
||||
|
||||
await act(async () => checkNowButton.click());
|
||||
await userEvent.click(checkNowButton);
|
||||
|
||||
// assert
|
||||
expect(checkIndex).toHaveBeenCalledTimes(1);
|
||||
|
@ -370,6 +557,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -384,7 +575,7 @@ describe('pattern', () => {
|
|||
name: 'View history',
|
||||
});
|
||||
|
||||
await act(async () => viewHistoryButton.click());
|
||||
await userEvent.click(viewHistoryButton);
|
||||
|
||||
// assert
|
||||
expect(fetchHistoricalResults).toHaveBeenCalledTimes(1);
|
||||
|
@ -444,6 +635,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -458,11 +653,11 @@ describe('pattern', () => {
|
|||
name: 'View history',
|
||||
});
|
||||
|
||||
await act(async () => viewHistoryButton.click());
|
||||
await userEvent.click(viewHistoryButton);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close this dialog' });
|
||||
|
||||
await act(async () => closeButton.click());
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
// assert
|
||||
expect(screen.queryByTestId('indexCheckFlyout')).not.toBeInTheDocument();
|
||||
|
@ -504,6 +699,10 @@ describe('pattern', () => {
|
|||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
|
@ -533,4 +732,342 @@ describe('pattern', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tour', () => {
|
||||
describe('when isTourActive and isFirstOpenNonEmptyPattern', () => {
|
||||
it('renders the tour near the first row history view button', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const rows = screen.getAllByRole('row');
|
||||
// skipping the first row which is the header
|
||||
const firstBodyRow = within(rows[1]);
|
||||
|
||||
const tourWrapper = await firstBodyRow.findByTestId('historicalResultsTour');
|
||||
|
||||
expect(
|
||||
within(tourWrapper).getByRole('button', { name: 'View history' })
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when accordion is collapsed', () => {
|
||||
it('hides the tour', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('historicalResultsTour')).toBeInTheDocument();
|
||||
|
||||
const accordionToggle = screen.getByRole('button', {
|
||||
name: 'Fail auditbeat-* hot (1) unmanaged (2) Incompatible fields 4 Indices checked 3 Indices 3 Size 17.9MB Docs 19,127',
|
||||
});
|
||||
|
||||
await userEvent.click(accordionToggle);
|
||||
|
||||
expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('when the tour close button is clicked', () => {
|
||||
it('invokes onDismissTour', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const onDismissTour = jest.fn();
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={onDismissTour}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const tourDialog = await screen.findByRole('dialog', {
|
||||
name: 'Introducing data quality history',
|
||||
});
|
||||
|
||||
const closeButton = within(tourDialog).getByRole('button', { name: 'Close' });
|
||||
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tour tryIt action is clicked', () => {
|
||||
it('opens the flyout with history tab and invokes onDismissTour', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const onDismissTour = jest.fn();
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={onDismissTour}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const tourDialog = await screen.findByRole('dialog', {
|
||||
name: 'Introducing data quality history',
|
||||
});
|
||||
|
||||
const tryItButton = within(tourDialog).getByRole('button', { name: 'Try it' });
|
||||
|
||||
await userEvent.click(tryItButton);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false'
|
||||
);
|
||||
expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when latest latest check flyout tab is opened', () => {
|
||||
it('hides the tour in listview and shows in flyout', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const onDismissTour = jest.fn();
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={onDismissTour}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const rows = screen.getAllByRole('row');
|
||||
// skipping the first row which is the header
|
||||
const firstBodyRow = within(rows[1]);
|
||||
|
||||
expect(await firstBodyRow.findByTestId('historicalResultsTour')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).toBeInTheDocument();
|
||||
|
||||
const checkNowButton = firstBodyRow.getByRole('button', {
|
||||
name: 'Check now',
|
||||
});
|
||||
await userEvent.click(checkNowButton);
|
||||
|
||||
expect(screen.getByTestId('indexCheckFlyout')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Latest Check' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false'
|
||||
);
|
||||
|
||||
expect(firstBodyRow.queryByTestId('historicalResultsTour')).not.toBeInTheDocument();
|
||||
|
||||
const tabWrapper = await screen.findByRole('tab', { name: 'History' });
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
tabWrapper.closest('[data-test-subj="historicalResultsTour"]')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
|
||||
expect(onDismissTour).not.toHaveBeenCalled();
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not isFirstOpenNonEmptyPattern', () => {
|
||||
it('does not render the tour', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={true}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={false}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not isTourActive', () => {
|
||||
it('does not render the tour', async () => {
|
||||
(useIlmExplain as jest.Mock).mockReturnValue({
|
||||
error: null,
|
||||
ilmExplain: auditbeatWithAllResults.ilmExplain,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
(useStats as jest.Mock).mockReturnValue({
|
||||
stats: auditbeatWithAllResults.stats,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<Pattern
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
chartSelectedIndex={null}
|
||||
setChartSelectedIndex={jest.fn()}
|
||||
indexNames={Object.keys(auditbeatWithAllResults.stats!)}
|
||||
pattern={pattern}
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
isFirstOpenNonEmptyPattern={true}
|
||||
onAccordionToggle={jest.fn()}
|
||||
/>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ import { getPageIndex } from './utils/get_page_index';
|
|||
import { useAbortControllerRef } from '../../../hooks/use_abort_controller_ref';
|
||||
import { useHistoricalResults } from './hooks/use_historical_results';
|
||||
import { HistoricalResultsContext } from './contexts/historical_results_context';
|
||||
import { HistoricalResultsTour } from './historical_results_tour';
|
||||
|
||||
const EMPTY_INDEX_NAMES: string[] = [];
|
||||
|
||||
|
@ -44,6 +45,11 @@ interface Props {
|
|||
patternRollup: PatternRollup | undefined;
|
||||
chartSelectedIndex: SelectedIndex | null;
|
||||
setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
|
||||
isTourActive: boolean;
|
||||
isFirstOpenNonEmptyPattern: boolean;
|
||||
onAccordionToggle: (patternName: string, isOpen: boolean, isEmpty: boolean) => void;
|
||||
onDismissTour: () => void;
|
||||
openPatternsUpdatedAt?: number;
|
||||
}
|
||||
|
||||
const PatternComponent: React.FC<Props> = ({
|
||||
|
@ -52,6 +58,11 @@ const PatternComponent: React.FC<Props> = ({
|
|||
patternRollup,
|
||||
chartSelectedIndex,
|
||||
setChartSelectedIndex,
|
||||
isTourActive,
|
||||
isFirstOpenNonEmptyPattern,
|
||||
onAccordionToggle,
|
||||
onDismissTour,
|
||||
openPatternsUpdatedAt,
|
||||
}) => {
|
||||
const { historicalResultsState, fetchHistoricalResults } = useHistoricalResults();
|
||||
const historicalResultsContextValue = useMemo(
|
||||
|
@ -124,6 +135,35 @@ const PatternComponent: React.FC<Props> = ({
|
|||
]
|
||||
);
|
||||
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
|
||||
|
||||
const isAccordionOpenRef = useRef(isAccordionOpen);
|
||||
useEffect(() => {
|
||||
isAccordionOpenRef.current = isAccordionOpen;
|
||||
}, [isAccordionOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// this use effect syncs isEmpty state with the parent component
|
||||
//
|
||||
// we do not add isAccordionOpen to the dependency array because
|
||||
// it is already handled by handleAccordionToggle
|
||||
// so we don't want to additionally trigger this useEffect when isAccordionOpen changes
|
||||
// because it's confusing and unnecessary
|
||||
// that's why we use ref here to keep separation of concerns
|
||||
onAccordionToggle(pattern, isAccordionOpenRef.current, items.length === 0);
|
||||
}, [items.length, onAccordionToggle, pattern]);
|
||||
|
||||
const handleAccordionToggle = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
const isEmpty = items.length === 0;
|
||||
setIsAccordionOpen(isOpen);
|
||||
onAccordionToggle(pattern, isOpen, isEmpty);
|
||||
},
|
||||
[items.length, onAccordionToggle, pattern]
|
||||
);
|
||||
|
||||
const firstRow = items[0];
|
||||
|
||||
const handleFlyoutClose = useCallback(() => {
|
||||
setExpandedIndexName(null);
|
||||
}, []);
|
||||
|
@ -153,6 +193,9 @@ const PatternComponent: React.FC<Props> = ({
|
|||
|
||||
const handleFlyoutViewCheckHistoryAction = useCallback(
|
||||
(indexName: string) => {
|
||||
if (isTourActive) {
|
||||
onDismissTour();
|
||||
}
|
||||
fetchHistoricalResults({
|
||||
abortController: flyoutViewCheckHistoryAbortControllerRef.current,
|
||||
indexName,
|
||||
|
@ -160,9 +203,16 @@ const PatternComponent: React.FC<Props> = ({
|
|||
setExpandedIndexName(indexName);
|
||||
setInitialFlyoutTabId(HISTORY_TAB_ID);
|
||||
},
|
||||
[fetchHistoricalResults, flyoutViewCheckHistoryAbortControllerRef]
|
||||
[fetchHistoricalResults, flyoutViewCheckHistoryAbortControllerRef, isTourActive, onDismissTour]
|
||||
);
|
||||
|
||||
const handleOpenFlyoutHistoryTab = useCallback(() => {
|
||||
const firstItemIndexName = firstRow?.indexName;
|
||||
if (firstItemIndexName) {
|
||||
handleFlyoutViewCheckHistoryAction(firstItemIndexName);
|
||||
}
|
||||
}, [firstRow?.indexName, handleFlyoutViewCheckHistoryAction]);
|
||||
|
||||
useEffect(() => {
|
||||
const newIndexNames = getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable });
|
||||
const newDocsCount = getPatternDocsCount({ indexNames: newIndexNames, stats });
|
||||
|
@ -270,7 +320,8 @@ const PatternComponent: React.FC<Props> = ({
|
|||
<HistoricalResultsContext.Provider value={historicalResultsContextValue}>
|
||||
<PatternAccordion
|
||||
id={patternComponentAccordionId}
|
||||
initialIsOpen={true}
|
||||
forceState={isAccordionOpen ? 'open' : 'closed'}
|
||||
onToggle={handleAccordionToggle}
|
||||
buttonElement="div"
|
||||
buttonContent={
|
||||
<PatternSummary
|
||||
|
@ -308,6 +359,34 @@ const PatternComponent: React.FC<Props> = ({
|
|||
|
||||
{!loading && error == null && (
|
||||
<div ref={containerRef}>
|
||||
<HistoricalResultsTour
|
||||
// this is a hack to force popover anchor position recalculation
|
||||
// when the first open non-empty pattern layout changes due to other
|
||||
// patterns being opened/closed
|
||||
// It's a bug on Eui side
|
||||
//
|
||||
// TODO: remove this hack when EUI popover is fixed
|
||||
// https://github.com/elastic/eui/issues/5226
|
||||
{...(isFirstOpenNonEmptyPattern && { key: openPatternsUpdatedAt })}
|
||||
anchorSelectorValue={pattern}
|
||||
onTryIt={handleOpenFlyoutHistoryTab}
|
||||
isOpen={
|
||||
isTourActive &&
|
||||
!isFlyoutVisible &&
|
||||
isFirstOpenNonEmptyPattern &&
|
||||
isAccordionOpen
|
||||
}
|
||||
onDismissTour={onDismissTour}
|
||||
// Only set zIndex when the tour is in list view (not in flyout)
|
||||
//
|
||||
// 1 less than the z-index of the left navigation
|
||||
// 5 less than the z-index of the timeline
|
||||
//
|
||||
//
|
||||
// TODO this hack should be removed when we properly set z-indexes
|
||||
// in the timeline and left navigation
|
||||
zIndex={998}
|
||||
/>
|
||||
<SummaryTable
|
||||
getTableColumns={getSummaryTableColumns}
|
||||
items={items}
|
||||
|
@ -334,6 +413,8 @@ const PatternComponent: React.FC<Props> = ({
|
|||
ilmExplain={ilmExplain}
|
||||
stats={stats}
|
||||
onClose={handleFlyoutClose}
|
||||
onDismissTour={onDismissTour}
|
||||
isTourActive={isTourActive}
|
||||
/>
|
||||
) : null}
|
||||
</HistoricalResultsContext.Provider>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { IndexCheckFlyout } from '.';
|
||||
|
@ -41,6 +41,8 @@ describe('IndexCheckFlyout', () => {
|
|||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
onDismissTour={jest.fn()}
|
||||
isTourActive={false}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
|
@ -97,6 +99,8 @@ describe('IndexCheckFlyout', () => {
|
|||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
|
@ -129,6 +133,8 @@ describe('IndexCheckFlyout', () => {
|
|||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
isTourActive={false}
|
||||
onDismissTour={jest.fn()}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
|
@ -175,6 +181,8 @@ describe('IndexCheckFlyout', () => {
|
|||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={jest.fn()}
|
||||
isTourActive={false}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
|
@ -207,4 +215,179 @@ describe('IndexCheckFlyout', () => {
|
|||
expect(screen.getByTestId('historicalResults')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tour guide', () => {
|
||||
describe('when in Latest Check tab and isTourActive', () => {
|
||||
it('should render the tour guide near history tab with proper data-tour-element attribute', async () => {
|
||||
const pattern = 'auditbeat-*';
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<TestHistoricalResultsProvider>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern={pattern}
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={jest.fn()}
|
||||
isTourActive={true}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const historyTab = screen.getByRole('tab', { name: 'History' });
|
||||
const latestCheckTab = screen.getByRole('tab', { name: 'Latest Check' });
|
||||
|
||||
expect(historyTab).toHaveAttribute('data-tour-element', `${pattern}-history-tab`);
|
||||
expect(latestCheckTab).not.toHaveAttribute('data-tour-element', `${pattern}-history-tab`);
|
||||
await waitFor(() =>
|
||||
expect(historyTab.closest('[data-test-subj="historicalResultsTour"]')).toBeInTheDocument()
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when the tour close button is clicked', () => {
|
||||
it('should invoke the dismiss tour callback', async () => {
|
||||
const onDismissTour = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<TestHistoricalResultsProvider>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={onDismissTour}
|
||||
isTourActive={true}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const dialogWrapper = await screen.findByRole('dialog', {
|
||||
name: 'Introducing data quality history',
|
||||
});
|
||||
|
||||
const closeButton = within(dialogWrapper).getByRole('button', { name: 'Close' });
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the tour TryIt button is clicked', () => {
|
||||
it('should switch to history tab and invoke onDismissTour', async () => {
|
||||
const onDismissTour = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<TestHistoricalResultsProvider>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={onDismissTour}
|
||||
isTourActive={true}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const dialogWrapper = await screen.findByRole('dialog', {
|
||||
name: 'Introducing data quality history',
|
||||
});
|
||||
|
||||
const tryItButton = within(dialogWrapper).getByRole('button', { name: 'Try it' });
|
||||
await userEvent.click(tryItButton);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalled();
|
||||
expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when manually switching to history tab', () => {
|
||||
it('should invoke onDismissTour', async () => {
|
||||
const onDismissTour = jest.fn();
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<TestHistoricalResultsProvider>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={onDismissTour}
|
||||
isTourActive={true}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const historyTab = screen.getByRole('tab', { name: 'History' });
|
||||
await userEvent.click(historyTab);
|
||||
|
||||
expect(onDismissTour).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not isTourActive', () => {
|
||||
it('should not render the tour guide', async () => {
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
<TestDataQualityProviders>
|
||||
<TestHistoricalResultsProvider>
|
||||
<IndexCheckFlyout
|
||||
ilmExplain={mockIlmExplain}
|
||||
indexName="auditbeat-custom-index-1"
|
||||
onClose={jest.fn()}
|
||||
pattern="auditbeat-*"
|
||||
patternRollup={auditbeatWithAllResults}
|
||||
stats={mockStats}
|
||||
initialSelectedTabId="latest_check"
|
||||
onDismissTour={jest.fn()}
|
||||
isTourActive={false}
|
||||
/>
|
||||
</TestHistoricalResultsProvider>
|
||||
</TestDataQualityProviders>
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument()
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: 'Introducing data quality history' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,8 +36,13 @@ import { HistoricalResults } from './historical_results';
|
|||
import { useHistoricalResultsContext } from '../contexts/historical_results_context';
|
||||
import { getFormattedCheckTime } from './utils/get_formatted_check_time';
|
||||
import { CHECK_NOW } from '../translations';
|
||||
import { HISTORY_TAB_ID, LATEST_CHECK_TAB_ID } from '../constants';
|
||||
import {
|
||||
HISTORICAL_RESULTS_TOUR_SELECTOR_KEY,
|
||||
HISTORY_TAB_ID,
|
||||
LATEST_CHECK_TAB_ID,
|
||||
} from '../constants';
|
||||
import { IndexCheckFlyoutTabId } from './types';
|
||||
import { HistoricalResultsTour } from '../historical_results_tour';
|
||||
|
||||
export interface Props {
|
||||
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
|
||||
|
@ -47,6 +52,8 @@ export interface Props {
|
|||
stats: Record<string, MeteringStatsIndex> | null;
|
||||
onClose: () => void;
|
||||
initialSelectedTabId: IndexCheckFlyoutTabId;
|
||||
onDismissTour: () => void;
|
||||
isTourActive: boolean;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
|
@ -68,6 +75,8 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
patternRollup,
|
||||
stats,
|
||||
onClose,
|
||||
onDismissTour,
|
||||
isTourActive,
|
||||
}) => {
|
||||
const didSwitchToLatestTabOnceRef = useRef(false);
|
||||
const { fetchHistoricalResults } = useHistoricalResultsContext();
|
||||
|
@ -90,12 +99,15 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
|
||||
const handleTabClick = useCallback(
|
||||
(tabId: IndexCheckFlyoutTabId) => {
|
||||
setSelectedTabId(tabId);
|
||||
if (tabId === HISTORY_TAB_ID) {
|
||||
if (isTourActive) {
|
||||
onDismissTour();
|
||||
}
|
||||
fetchHistoricalResults({
|
||||
abortController: fetchHistoricalResultsAbortControllerRef.current,
|
||||
indexName,
|
||||
});
|
||||
setSelectedTabId(tabId);
|
||||
}
|
||||
|
||||
if (tabId === LATEST_CHECK_TAB_ID) {
|
||||
|
@ -110,7 +122,6 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
formatNumber,
|
||||
});
|
||||
}
|
||||
setSelectedTabId(tabId);
|
||||
}
|
||||
},
|
||||
[
|
||||
|
@ -122,6 +133,8 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
formatNumber,
|
||||
httpFetch,
|
||||
indexName,
|
||||
isTourActive,
|
||||
onDismissTour,
|
||||
pattern,
|
||||
]
|
||||
);
|
||||
|
@ -149,6 +162,10 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
selectedTabId,
|
||||
]);
|
||||
|
||||
const handleSelectHistoryTab = useCallback(() => {
|
||||
handleTabClick(HISTORY_TAB_ID);
|
||||
}, [handleTabClick]);
|
||||
|
||||
const renderTabs = useMemo(
|
||||
() =>
|
||||
tabs.map((tab, index) => {
|
||||
|
@ -157,12 +174,15 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
onClick={() => handleTabClick(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
key={index}
|
||||
{...(tab.id === HISTORY_TAB_ID && {
|
||||
[HISTORICAL_RESULTS_TOUR_SELECTOR_KEY]: `${pattern}-history-tab`,
|
||||
})}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
);
|
||||
}),
|
||||
[handleTabClick, selectedTabId]
|
||||
[handleTabClick, pattern, selectedTabId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -195,12 +215,20 @@ export const IndexCheckFlyoutComponent: React.FC<Props> = ({
|
|||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{selectedTabId === LATEST_CHECK_TAB_ID ? (
|
||||
<LatestResults
|
||||
indexName={indexName}
|
||||
stats={stats}
|
||||
ilmExplain={ilmExplain}
|
||||
patternRollup={patternRollup}
|
||||
/>
|
||||
<>
|
||||
<LatestResults
|
||||
indexName={indexName}
|
||||
stats={stats}
|
||||
ilmExplain={ilmExplain}
|
||||
patternRollup={patternRollup}
|
||||
/>
|
||||
<HistoricalResultsTour
|
||||
anchorSelectorValue={`${pattern}-history-tab`}
|
||||
onTryIt={handleSelectHistoryTab}
|
||||
isOpen={isTourActive}
|
||||
onDismissTour={onDismissTour}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<HistoricalResults indexName={indexName} />
|
||||
)}
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface Props {
|
|||
pattern: string;
|
||||
onCheckNowAction: (indexName: string) => void;
|
||||
onViewHistoryAction: (indexName: string) => void;
|
||||
firstIndexName?: string;
|
||||
}) => Array<EuiBasicTableColumn<IndexSummaryTableItem>>;
|
||||
items: IndexSummaryTableItem[];
|
||||
pageIndex: number;
|
||||
|
@ -66,6 +67,7 @@ const SummaryTableComponent: React.FC<Props> = ({
|
|||
pattern,
|
||||
onCheckNowAction,
|
||||
onViewHistoryAction,
|
||||
firstIndexName: items[0]?.indexName,
|
||||
}),
|
||||
[
|
||||
getTableColumns,
|
||||
|
@ -75,6 +77,7 @@ const SummaryTableComponent: React.FC<Props> = ({
|
|||
pattern,
|
||||
onCheckNowAction,
|
||||
onViewHistoryAction,
|
||||
items,
|
||||
]
|
||||
);
|
||||
const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []);
|
||||
|
|
|
@ -197,6 +197,60 @@ describe('helpers', () => {
|
|||
|
||||
expect(onViewHistoryAction).toBeCalledWith(indexSummaryTableItem.indexName);
|
||||
});
|
||||
|
||||
test('adds data-tour-element attribute to the first view history button', () => {
|
||||
const pattern = 'auditbeat-*';
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
pattern,
|
||||
onCheckNowAction: jest.fn(),
|
||||
onViewHistoryAction: jest.fn(),
|
||||
firstIndexName: indexName,
|
||||
});
|
||||
|
||||
const expandActionRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[1] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
{expandActionRender != null && expandActionRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByLabelText(VIEW_HISTORY);
|
||||
expect(button).toHaveAttribute('data-tour-element', pattern);
|
||||
});
|
||||
|
||||
test('doesn`t add data-tour-element attribute to non-first view history buttons', () => {
|
||||
const pattern = 'auditbeat-*';
|
||||
const columns = getSummaryTableColumns({
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
isILMAvailable,
|
||||
pattern,
|
||||
onCheckNowAction: jest.fn(),
|
||||
onViewHistoryAction: jest.fn(),
|
||||
firstIndexName: 'another-index',
|
||||
});
|
||||
|
||||
const expandActionRender = (
|
||||
(columns[0] as EuiTableActionsColumnType<IndexSummaryTableItem>)
|
||||
.actions[1] as CustomItemAction<IndexSummaryTableItem>
|
||||
).render;
|
||||
|
||||
render(
|
||||
<TestExternalProviders>
|
||||
{expandActionRender != null && expandActionRender(indexSummaryTableItem, true)}
|
||||
</TestExternalProviders>
|
||||
);
|
||||
|
||||
const button = screen.getByLabelText(VIEW_HISTORY);
|
||||
expect(button).not.toHaveAttribute('data-tour-element');
|
||||
});
|
||||
});
|
||||
|
||||
describe('incompatible render()', () => {
|
||||
|
|
|
@ -37,6 +37,7 @@ import { IndexResultBadge } from '../../index_result_badge';
|
|||
import { Stat } from '../../../../../stat';
|
||||
import { getIndexResultToolTip } from '../../utils/get_index_result_tooltip';
|
||||
import { CHECK_NOW } from '../../translations';
|
||||
import { HISTORICAL_RESULTS_TOUR_SELECTOR_KEY } from '../../constants';
|
||||
|
||||
const ProgressContainer = styled.div`
|
||||
width: 150px;
|
||||
|
@ -102,6 +103,7 @@ export const getSummaryTableColumns = ({
|
|||
pattern,
|
||||
onCheckNowAction,
|
||||
onViewHistoryAction,
|
||||
firstIndexName,
|
||||
}: {
|
||||
formatBytes: (value: number | undefined) => string;
|
||||
formatNumber: (value: number | undefined) => string;
|
||||
|
@ -109,6 +111,7 @@ export const getSummaryTableColumns = ({
|
|||
pattern: string;
|
||||
onCheckNowAction: (indexName: string) => void;
|
||||
onViewHistoryAction: (indexName: string) => void;
|
||||
firstIndexName?: string;
|
||||
}): Array<EuiBasicTableColumn<IndexSummaryTableItem>> => [
|
||||
{
|
||||
name: i18n.ACTIONS,
|
||||
|
@ -132,12 +135,16 @@ export const getSummaryTableColumns = ({
|
|||
{
|
||||
name: i18n.VIEW_HISTORY,
|
||||
render: (item) => {
|
||||
const isFirstIndexName = firstIndexName === item.indexName;
|
||||
return (
|
||||
<EuiToolTip content={i18n.VIEW_HISTORY}>
|
||||
<EuiButtonIcon
|
||||
iconType="clockCounter"
|
||||
aria-label={i18n.VIEW_HISTORY}
|
||||
onClick={() => onViewHistoryAction(item.indexName)}
|
||||
{...(isFirstIndexName && {
|
||||
[HISTORICAL_RESULTS_TOUR_SELECTOR_KEY]: pattern,
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
|
|
@ -166,3 +166,21 @@ export const auditbeatWithAllResults: PatternRollup = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyAuditbeatPatternRollup: PatternRollup = {
|
||||
docsCount: 0,
|
||||
error: null,
|
||||
ilmExplain: {},
|
||||
ilmExplainPhaseCounts: {
|
||||
hot: 0,
|
||||
warm: 0,
|
||||
cold: 0,
|
||||
frozen: 0,
|
||||
unmanaged: 0,
|
||||
},
|
||||
indices: 0,
|
||||
pattern: 'auditbeat-*',
|
||||
results: {},
|
||||
sizeInBytes: 0,
|
||||
stats: {},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue