Skip to content

Commit

Permalink
[Security Solution][DQD] Add historical results tour guide
Browse files Browse the repository at this point in the history
addresses elastic#195971
  • Loading branch information
kapral18 committed Oct 15, 2024
1 parent da63469 commit fad9879
Show file tree
Hide file tree
Showing 14 changed files with 1,058 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) =>
Expand Down Expand Up @@ -58,6 +60,7 @@ const defaultProps: Props = {
describe('IndicesDetails', () => {
beforeEach(async () => {
jest.clearAllMocks();
localStorage.removeItem(HISTORICAL_RESULTS_TOUR_IS_ACTIVE_STORAGE_KEY);

render(
<TestExternalProviders>
Expand All @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -34,19 +35,62 @@ const IndicesDetailsComponent: React.FC<Props> = ({
const { patternRollups, patternIndexNames } = useResultsRollupContext();
const { patterns } = useDataQualityContext();

const [isTourActive, setIsTourActive] = useState<boolean>(() => {
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<Array<{ name: string; isOpen: boolean }>>(() => {
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 (
<div data-test-subj="indicesDetails">
{patterns.map((pattern) => (
<StyledPatternWrapperFlexItem grow={false} key={pattern}>
<Pattern
indexNames={patternIndexNames[pattern]}
pattern={pattern}
patternRollup={patternRollups[pattern]}
chartSelectedIndex={chartSelectedIndex}
setChartSelectedIndex={setChartSelectedIndex}
/>
</StyledPatternWrapperFlexItem>
))}
{useMemo(
() =>
patterns.map((pattern) => (
<StyledPatternWrapperFlexItem grow={false} key={pattern}>
<Pattern
indexNames={patternIndexNames[pattern]}
pattern={pattern}
patternRollup={patternRollups[pattern]}
chartSelectedIndex={chartSelectedIndex}
setChartSelectedIndex={setChartSelectedIndex}
isTourActive={isTourActive}
isFirstOpenPattern={pattern === firstOpenPattern}
onAccordionToggle={handleAccordionToggle}
onDismissTour={handleDismissTour}
/>
</StyledPatternWrapperFlexItem>
)),
[
chartSelectedIndex,
firstOpenPattern,
handleAccordionToggle,
handleDismissTour,
isTourActive,
patternIndexNames,
patternRollups,
patterns,
setChartSelectedIndex,
]
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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()
);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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<HistoricalResultsTourProps> = ({
anchorSelectorValue,
onTryIt,
isOpen,
onDismissTour,
}) => {
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}
footerAction={[
<EuiButtonEmpty size="xs" color="text" onClick={onDismissTour}>
{CLOSE}
</EuiButtonEmpty>,
<EuiButton color="success" size="s" onClick={onTryIt}>
{TRY_IT}
</EuiButton>,
]}
/>
);
};
Loading

0 comments on commit fad9879

Please sign in to comment.