From fad9879d72cc810b485cb1cac65168a0a62dfede Mon Sep 17 00:00:00 2001 From: Karen Grigoryan Date: Mon, 14 Oct 2024 14:31:15 +0200 Subject: [PATCH] [Security Solution][DQD] Add historical results tour guide addresses #195971 --- .../indices_details/constants.ts | 9 + .../indices_details/index.test.tsx | 65 ++- .../indices_details/index.tsx | 68 ++- .../indices_details/pattern/constants.ts | 2 + .../historical_results_tour/index.test.tsx | 105 +++++ .../pattern/historical_results_tour/index.tsx | 77 ++++ .../historical_results_tour/translations.ts | 30 ++ .../indices_details/pattern/index.test.tsx | 393 +++++++++++++++++- .../indices_details/pattern/index.tsx | 43 +- .../pattern/index_check_flyout/index.test.tsx | 185 ++++++++- .../pattern/index_check_flyout/index.tsx | 48 ++- .../pattern/summary_table/index.tsx | 5 + .../summary_table/utils/columns.test.tsx | 54 +++ .../pattern/summary_table/utils/columns.tsx | 7 + 14 files changed, 1058 insertions(+), 33 deletions(-) create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/constants.ts create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.test.tsx create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.tsx create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/translations.ts diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/constants.ts new file mode 100644 index 0000000000000..1e4a2ce66ecc1 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/constants.ts @@ -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_ACTIVE_STORAGE_KEY = + 'securitySolution.dataQualityDashboard.historicalResultsTour.v8.16.isActive'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.test.tsx index d5aaa1eea19ae..58aa22cf533ff 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.test.tsx @@ -6,7 +6,7 @@ */ 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'; @@ -19,6 +19,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_ACTIVE_STORAGE_KEY } from './constants'; const defaultBytesFormat = '0,0.[0]b'; const formatBytes = (value: number | undefined) => @@ -58,6 +60,7 @@ const defaultProps: Props = { describe('IndicesDetails', () => { beforeEach(async () => { jest.clearAllMocks(); + localStorage.removeItem(HISTORICAL_RESULTS_TOUR_IS_ACTIVE_STORAGE_KEY); render( @@ -74,10 +77,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 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[0]); + + 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_ACTIVE_STORAGE_KEY)).toEqual( + 'false' + ); }); }); + + describe('when the first pattern is toggled', () => { + test('it renders the tour wrapping view history button of first row of second pattern', async () => { + const firstPatternAccordionWrapper = await screen.findByTestId( + `${patterns[0]}PatternPanel` + ); + const accordionToggle = within(firstPatternAccordionWrapper).getByRole('button', { + name: /Pass/, + }); + await userEvent.click(accordionToggle); + + const secondPatternAccordionWrapper = screen.getByTestId(`${patterns[1]}PatternPanel`); + const historicalResultsWrapper = await within(secondPatternAccordionWrapper).findByTestId( + 'historicalResultsTour' + ); + const button = within(historicalResultsWrapper).getByRole('button', { + name: 'View history', + }); + expect(button).toHaveAttribute('data-tour-element', patterns[1]); + + expect( + screen.getByRole('dialog', { name: 'Introducing data quality history' }) + ).toBeInTheDocument(); + }, 10000); + }); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.tsx index fd565d8fc7637..745ca4e183717 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/index.tsx @@ -6,13 +6,14 @@ */ import { EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useState, useMemo, useCallback } 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 { HISTORICAL_RESULTS_TOUR_IS_ACTIVE_STORAGE_KEY } from './constants'; const StyledPatternWrapperFlexItem = styled(EuiFlexItem)` margin-bottom: ${({ theme }) => theme.eui.euiSize}; @@ -34,19 +35,62 @@ const IndicesDetailsComponent: React.FC = ({ const { patternRollups, patternIndexNames } = useResultsRollupContext(); const { patterns } = useDataQualityContext(); + const [isTourActive, setIsTourActive] = useState(() => { + const isActive = localStorage.getItem(HISTORICAL_RESULTS_TOUR_IS_ACTIVE_STORAGE_KEY); + return isActive !== 'false'; + }); + + const handleDismissTour = useCallback(() => { + setIsTourActive(false); + localStorage.setItem(HISTORICAL_RESULTS_TOUR_IS_ACTIVE_STORAGE_KEY, 'false'); + }, []); + + const [openPatterns, setOpenPatterns] = useState>(() => { + return patterns.map((pattern) => ({ name: pattern, isOpen: true })); + }); + + const handleAccordionToggle = useCallback((patternName: string, isOpen: boolean) => { + setOpenPatterns((prevOpenPatterns) => { + return prevOpenPatterns.map((p) => (p.name === patternName ? { ...p, isOpen } : p)); + }); + }, []); + + const firstOpenPattern = useMemo( + () => openPatterns.find((pattern) => pattern.isOpen)?.name, + [openPatterns] + ); + return (
- {patterns.map((pattern) => ( - - - - ))} + {useMemo( + () => + patterns.map((pattern) => ( + + + + )), + [ + chartSelectedIndex, + firstOpenPattern, + handleAccordionToggle, + handleDismissTour, + isTourActive, + patternIndexNames, + patternRollups, + patterns, + setChartSelectedIndex, + ] + )}
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts index 4bab5938cf98b..a02eccb3e81a4 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/constants.ts @@ -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'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.test.tsx new file mode 100644 index 0000000000000..53f2e059072c8 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.test.tsx @@ -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( + + ); + + 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 = `
`; + }); + + describe('when isOpen is true', () => { + const onTryIt = jest.fn(); + const onDismissTour = jest.fn(); + beforeEach(() => { + render( + + ); + }); + 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( + + ); + + await waitFor(() => + expect(screen.queryByText(INTRODUCING_DATA_QUALITY_HISTORY)).not.toBeInTheDocument() + ); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.tsx new file mode 100644 index 0000000000000..c3f2181665efa --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/index.tsx @@ -0,0 +1,77 @@ +/* + * 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'; + +interface HistoricalResultsTourProps { + anchorSelectorValue: string; + isOpen: boolean; + onTryIt: () => void; + onDismissTour: () => void; +} + +const StyledText = styled(EuiText)` + margin-block-start: -10px; +`; + +export const HistoricalResultsTour: FC = ({ + anchorSelectorValue, + onTryIt, + isOpen, + onDismissTour, +}) => { + const [anchorElement, setAnchorElement] = useState(); + + useEffect(() => { + const element = document.querySelector( + `[${HISTORICAL_RESULTS_TOUR_SELECTOR_KEY}="${anchorSelectorValue}"]` + ); + + if (!element) { + return; + } + + setAnchorElement(element); + }, [anchorSelectorValue]); + + if (!isOpen || !anchorElement) { + return null; + } + + return ( + +

{VIEW_PAST_RESULTS}

+ + } + data-test-subj="historicalResultsTour" + isStepOpen={isOpen} + minWidth={283} + onFinish={onDismissTour} + step={1} + stepsTotal={1} + title={INTRODUCING_DATA_QUALITY_HISTORY} + anchorPosition="rightUp" + repositionOnScroll + anchor={anchorElement} + footerAction={[ + + {CLOSE} + , + + {TRY_IT} + , + ]} + /> + ); +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/translations.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/translations.ts new file mode 100644 index 0000000000000..d8f81aa288baa --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/historical_results_tour/translations.ts @@ -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', + } +); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx index a165378df80ed..1f077d6a829e1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { act, render, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import { TestDataQualityProviders, @@ -19,6 +19,7 @@ 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 +82,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} />
@@ -107,6 +112,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={undefined} pattern={'remote:*'} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -127,6 +136,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={undefined} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -155,6 +168,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -182,6 +199,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -215,6 +236,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -248,6 +273,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -292,6 +321,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -306,7 +339,7 @@ describe('pattern', () => { name: 'Check now', }); - await act(async () => checkNowButton.click()); + await userEvent.click(checkNowButton); // assert expect(checkIndex).toHaveBeenCalledTimes(1); @@ -370,6 +403,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -384,7 +421,7 @@ describe('pattern', () => { name: 'View history', }); - await act(async () => viewHistoryButton.click()); + await userEvent.click(viewHistoryButton); // assert expect(fetchHistoricalResults).toHaveBeenCalledTimes(1); @@ -444,6 +481,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -458,11 +499,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 +545,10 @@ describe('pattern', () => { setChartSelectedIndex={jest.fn()} indexNames={Object.keys(auditbeatWithAllResults.stats!)} pattern={pattern} + isTourActive={false} + onDismissTour={jest.fn()} + isFirstOpenPattern={false} + onAccordionToggle={jest.fn()} /> @@ -533,4 +578,342 @@ describe('pattern', () => { }); }); }); + + describe('Tour', () => { + describe('when isTourActive and isFirstOpenPattern', () => { + 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( + + + + + + ); + + 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( + + + + + + ); + + 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(); + }); + }); + + 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( + + + + + + ); + + 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( + + + + + + ); + + 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( + + + + + + ); + + 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 isFirstOpenPattern', () => { + 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( + + + + + + ); + + 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( + + + + + + ); + + expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument(); + }); + }); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx index 30c4aa8755a9c..dabc462a40966 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index.tsx @@ -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,10 @@ interface Props { patternRollup: PatternRollup | undefined; chartSelectedIndex: SelectedIndex | null; setChartSelectedIndex: (selectedIndex: SelectedIndex | null) => void; + isTourActive: boolean; + isFirstOpenPattern: boolean; + onAccordionToggle: (patternName: string, isOpen: boolean) => void; + onDismissTour: () => void; } const PatternComponent: React.FC = ({ @@ -52,6 +57,10 @@ const PatternComponent: React.FC = ({ patternRollup, chartSelectedIndex, setChartSelectedIndex, + isTourActive, + isFirstOpenPattern, + onAccordionToggle, + onDismissTour, }) => { const { historicalResultsState, fetchHistoricalResults } = useHistoricalResults(); const historicalResultsContextValue = useMemo( @@ -124,6 +133,18 @@ const PatternComponent: React.FC = ({ ] ); + const [isAccordionOpen, setIsAccordionOpen] = useState(true); + + const handleAccordionToggle = useCallback( + (isOpen: boolean) => { + setIsAccordionOpen(isOpen); + onAccordionToggle(pattern, isOpen); + }, + [onAccordionToggle, pattern] + ); + + const firstRow = items[0]; + const handleFlyoutClose = useCallback(() => { setExpandedIndexName(null); }, []); @@ -153,6 +174,9 @@ const PatternComponent: React.FC = ({ const handleFlyoutViewCheckHistoryAction = useCallback( (indexName: string) => { + if (isTourActive) { + onDismissTour(); + } fetchHistoricalResults({ abortController: flyoutViewCheckHistoryAbortControllerRef.current, indexName, @@ -160,9 +184,16 @@ const PatternComponent: React.FC = ({ 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 }); @@ -271,6 +302,7 @@ const PatternComponent: React.FC = ({ = ({ {!loading && error == null && (
+ = ({ onCheckNowAction={handleFlyoutCheckNowAndExpandAction} onViewHistoryAction={handleFlyoutViewCheckHistoryAction} sorting={sorting} + firstIndexName={items[0]?.indexName} />
)} @@ -334,6 +373,8 @@ const PatternComponent: React.FC = ({ ilmExplain={ilmExplain} stats={stats} onClose={handleFlyoutClose} + onDismissTour={onDismissTour} + isTourActive={isTourActive} /> ) : null} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx index 7b63f712a99da..e73fd4c2d610d 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.test.tsx @@ -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} /> @@ -97,6 +99,8 @@ describe('IndexCheckFlyout', () => { patternRollup={auditbeatWithAllResults} stats={mockStats} initialSelectedTabId="latest_check" + isTourActive={false} + onDismissTour={jest.fn()} /> @@ -129,6 +133,8 @@ describe('IndexCheckFlyout', () => { patternRollup={auditbeatWithAllResults} stats={mockStats} initialSelectedTabId="latest_check" + isTourActive={false} + onDismissTour={jest.fn()} /> @@ -175,6 +181,8 @@ describe('IndexCheckFlyout', () => { patternRollup={auditbeatWithAllResults} stats={mockStats} initialSelectedTabId="latest_check" + onDismissTour={jest.fn()} + isTourActive={false} /> @@ -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( + + + + + + + + ); + + 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( + + + + + + + + ); + + 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( + + + + + + + + ); + + 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( + + + + + + + + ); + + 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( + + + + + + + + ); + + await waitFor(() => + expect(screen.queryByTestId('historicalResultsTour')).not.toBeInTheDocument() + ); + + expect( + screen.queryByRole('dialog', { name: 'Introducing data quality history' }) + ).not.toBeInTheDocument(); + }); + }); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx index f298af704307d..b6dcf850d15b0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/index.tsx @@ -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 | null; @@ -47,6 +52,8 @@ export interface Props { stats: Record | null; onClose: () => void; initialSelectedTabId: IndexCheckFlyoutTabId; + onDismissTour: () => void; + isTourActive: boolean; } const tabs = [ @@ -68,6 +75,8 @@ export const IndexCheckFlyoutComponent: React.FC = ({ patternRollup, stats, onClose, + onDismissTour, + isTourActive, }) => { const didSwitchToLatestTabOnceRef = useRef(false); const { fetchHistoricalResults } = useHistoricalResultsContext(); @@ -90,12 +99,15 @@ export const IndexCheckFlyoutComponent: React.FC = ({ 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 = ({ formatNumber, }); } - setSelectedTabId(tabId); } }, [ @@ -122,6 +133,8 @@ export const IndexCheckFlyoutComponent: React.FC = ({ formatNumber, httpFetch, indexName, + isTourActive, + onDismissTour, pattern, ] ); @@ -149,6 +162,10 @@ export const IndexCheckFlyoutComponent: React.FC = ({ 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 = ({ 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} ); }), - [handleTabClick, selectedTabId] + [handleTabClick, pattern, selectedTabId] ); return ( @@ -195,12 +215,20 @@ export const IndexCheckFlyoutComponent: React.FC = ({ {selectedTabId === LATEST_CHECK_TAB_ID ? ( - + <> + + + ) : ( )} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx index fa574362e7d9b..827301170d417 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/index.tsx @@ -30,6 +30,7 @@ export interface Props { pattern: string; onCheckNowAction: (indexName: string) => void; onViewHistoryAction: (indexName: string) => void; + firstIndexName?: string; }) => Array>; items: IndexSummaryTableItem[]; pageIndex: number; @@ -41,6 +42,7 @@ export interface Props { sorting: SortConfig; onCheckNowAction: (indexName: string) => void; onViewHistoryAction: (indexName: string) => void; + firstIndexName?: string; } const SummaryTableComponent: React.FC = ({ @@ -55,6 +57,7 @@ const SummaryTableComponent: React.FC = ({ sorting, onCheckNowAction, onViewHistoryAction, + firstIndexName, }) => { const { isILMAvailable, formatBytes, formatNumber } = useDataQualityContext(); const columns = useMemo( @@ -66,6 +69,7 @@ const SummaryTableComponent: React.FC = ({ pattern, onCheckNowAction, onViewHistoryAction, + firstIndexName, }), [ getTableColumns, @@ -75,6 +79,7 @@ const SummaryTableComponent: React.FC = ({ pattern, onCheckNowAction, onViewHistoryAction, + firstIndexName, ] ); const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx index eda93c45f3b4f..bffd0c7fb91de 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.test.tsx @@ -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) + .actions[1] as CustomItemAction + ).render; + + render( + + {expandActionRender != null && expandActionRender(indexSummaryTableItem, true)} + + ); + + 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) + .actions[1] as CustomItemAction + ).render; + + render( + + {expandActionRender != null && expandActionRender(indexSummaryTableItem, true)} + + ); + + const button = screen.getByLabelText(VIEW_HISTORY); + expect(button).not.toHaveAttribute('data-tour-element'); + }); }); describe('incompatible render()', () => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx index c930d47babc2e..832ba71d26af8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/summary_table/utils/columns.tsx @@ -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> => [ { name: i18n.ACTIONS, @@ -132,12 +135,16 @@ export const getSummaryTableColumns = ({ { name: i18n.VIEW_HISTORY, render: (item) => { + const isFirstIndexName = firstIndexName === item.indexName; return ( onViewHistoryAction(item.indexName)} + {...(isFirstIndexName && { + [HISTORICAL_RESULTS_TOUR_SELECTOR_KEY]: pattern, + })} /> );