Skip to content

Commit

Permalink
Assistant refactor (#162079)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Jul 25, 2023
1 parent f1fca32 commit 06fabab
Show file tree
Hide file tree
Showing 18 changed files with 1,082 additions and 284 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render } from '@testing-library/react';
import { AssistantHeader } from '.';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { alertConvo, emptyWelcomeConvo } from '../../mock/conversation';

const testProps = {
currentConversation: emptyWelcomeConvo,
currentTitle: {
title: 'Test Title',
titleIcon: 'logoSecurity',
},
docLinks: {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
DOC_LINK_VERSION: 'master',
},
isDisabled: false,
isSettingsModalVisible: false,
onConversationSelected: jest.fn(),
onToggleShowAnonymizedValues: jest.fn(),
selectedConversationId: emptyWelcomeConvo.id,
setIsSettingsModalVisible: jest.fn(),
setSelectedConversationId: jest.fn(),
showAnonymizedValues: false,
};

describe('AssistantHeader', () => {
it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => {
const { getByText, getByTestId } = render(<AssistantHeader {...testProps} />, {
wrapper: TestProviders,
});
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
});

it('showAnonymizedValues is not checked when currentConversation.replacements is empty', () => {
const { getByText, getByTestId } = render(
<AssistantHeader
{...testProps}
currentConversation={{ ...emptyWelcomeConvo, replacements: {} }}
/>,
{
wrapper: TestProviders,
}
);
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
});

it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => {
const { getByTestId } = render(
<AssistantHeader
{...testProps}
currentConversation={alertConvo}
selectedConversationId={alertConvo.id}
/>,
{
wrapper: TestProviders,
}
);
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
});

it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => {
const { getByTestId } = render(
<AssistantHeader
{...testProps}
currentConversation={alertConvo}
selectedConversationId={alertConvo.id}
showAnonymizedValues
/>,
{
wrapper: TestProviders,
}
);
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
EuiToolTip,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { DocLinksStart } from '@kbn/core-doc-links-browser';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
import { ConversationSelector } from '../conversations/conversation_selector';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import * as i18n from '../translations';

interface OwnProps {
currentConversation: Conversation;
currentTitle: { title: string | JSX.Element; titleIcon: string };
defaultConnectorId?: string;
defaultProvider?: OpenAiProviderType;
docLinks: Omit<DocLinksStart, 'links'>;
isDisabled: boolean;
isSettingsModalVisible: boolean;
onConversationSelected: (cId: string) => void;
onToggleShowAnonymizedValues: (e: EuiSwitchEvent) => void;
selectedConversationId: string;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
shouldDisableKeyboardShortcut?: () => boolean;
showAnonymizedValues: boolean;
}

type Props = OwnProps;
/**
* Renders the header of the Elastic AI Assistant.
* Provide a user interface for selecting and managing conversations,
* toggling the display of anonymized values, and accessing the assistant settings.
*/
export const AssistantHeader: React.FC<Props> = ({
currentConversation,
currentTitle,
defaultConnectorId,
defaultProvider,
docLinks,
isDisabled,
isSettingsModalVisible,
onConversationSelected,
onToggleShowAnonymizedValues,
selectedConversationId,
setIsSettingsModalVisible,
setSelectedConversationId,
shouldDisableKeyboardShortcut,
showAnonymizedValues,
}) => {
const showAnonymizedValuesChecked = useMemo(
() =>
currentConversation.replacements != null &&
Object.keys(currentConversation.replacements).length > 0 &&
showAnonymizedValues,
[currentConversation.replacements, showAnonymizedValues]
);
return (
<>
<EuiFlexGroup
css={css`
width: 100%;
`}
alignItems={'center'}
justifyContent={'spaceBetween'}
>
<EuiFlexItem grow={false}>
<AssistantTitle {...currentTitle} docLinks={docLinks} />
</EuiFlexItem>

<EuiFlexItem
grow={false}
css={css`
width: 335px;
`}
>
<ConversationSelector
defaultConnectorId={defaultConnectorId}
defaultProvider={defaultProvider}
selectedConversationId={selectedConversationId}
onConversationSelected={onConversationSelected}
shouldDisableKeyboardShortcut={shouldDisableKeyboardShortcut}
isDisabled={isDisabled}
/>

<>
<EuiSpacer size={'s'} />
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.SHOW_ANONYMIZED_TOOLTIP}
position="left"
repositionOnScroll={true}
>
<EuiSwitch
data-test-subj="showAnonymizedValues"
checked={showAnonymizedValuesChecked}
compressed={true}
disabled={currentConversation.replacements == null}
label={i18n.SHOW_ANONYMIZED}
onChange={onToggleShowAnonymizedValues}
/>
</EuiToolTip>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<AssistantSettingsButton
defaultConnectorId={defaultConnectorId}
defaultProvider={defaultProvider}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}
selectedConversation={currentConversation}
setIsSettingsModalVisible={setIsSettingsModalVisible}
setSelectedConversationId={setSelectedConversationId}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin={'m'} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { AssistantTitle } from '.';
import { TestProviders } from '../../mock/test_providers/test_providers';

const testProps = {
title: 'Test Title',
titleIcon: 'globe',
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
};
describe('AssistantTitle', () => {
it('the component renders correctly with valid props', () => {
const { getByText, container } = render(<AssistantTitle {...testProps} />);
expect(getByText('Test Title')).toBeInTheDocument();
expect(container.querySelector('[data-euiicon-type="globe"]')).not.toBeNull();
});

it('clicking on the popover button opens the popover with the correct link', () => {
const { getByTestId, queryByTestId } = render(<AssistantTitle {...testProps} />, {
wrapper: TestProviders,
});
expect(queryByTestId('tooltipContent')).not.toBeInTheDocument();
fireEvent.click(getByTestId('tooltipIcon'));
expect(getByTestId('tooltipContent')).toBeInTheDocument();
expect(getByTestId('externalDocumentationLink')).toHaveAttribute(
'href',
'https://www.elastic.co/guide/en/security/7.15/security-assistant.html'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { FunctionComponent, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
Expand All @@ -20,10 +20,15 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from '../translations';

export const AssistantTitle: FunctionComponent<{
currentTitle: { title: string | JSX.Element; titleIcon: string };
/**
* Renders a header title with an icon, a tooltip button, and a popover with
* information about the assistant feature and access to documentation.
*/
export const AssistantTitle: React.FC<{
title: string | JSX.Element;
titleIcon: string;
docLinks: Omit<DocLinksStart, 'links'>;
}> = ({ currentTitle, docLinks }) => {
}> = ({ title, titleIcon, docLinks }) => {
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`;

Expand Down Expand Up @@ -54,21 +59,24 @@ export const AssistantTitle: FunctionComponent<{
),
[documentationLink]
);

const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = () => setIsPopoverOpen((isOpen: boolean) => !isOpen);
const closePopover = () => setIsPopoverOpen(false);
const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen: boolean) => !isOpen), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);

return (
<EuiModalHeaderTitle>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={currentTitle.titleIcon} size="xl" />
<EuiIcon data-test-subj="titleIcon" type={titleIcon} size="xl" />
</EuiFlexItem>
<EuiFlexItem grow={false}>{currentTitle.title}</EuiFlexItem>
<EuiFlexItem grow={false}>{title}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
aria-label={i18n.TOOLTIP_ARIA_LABEL}
data-test-subj="tooltipIcon"
iconSize="l"
iconType="iInCircle"
onClick={onButtonClick}
Expand All @@ -78,7 +86,7 @@ export const AssistantTitle: FunctionComponent<{
closePopover={closePopover}
anchorPosition="upCenter"
>
<EuiText grow={false} css={{ maxWidth: '400px' }}>
<EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}>
<h4>{i18n.TOOLTIP_TITLE}</h4>
<p>{content}</p>
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { BlockBotCallToAction } from './cta';
import { HttpSetup } from '@kbn/core-http-browser';

const testProps = {
connectorPrompt: <div>{'Connector Prompt'}</div>,
http: { basePath: { get: jest.fn(() => 'http://localhost:5601') } } as unknown as HttpSetup,
isAssistantEnabled: false,
isWelcomeSetup: false,
};

describe('BlockBotCallToAction', () => {
it('UpgradeButtons is rendered when isAssistantEnabled is false and isWelcomeSetup is false', () => {
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...testProps} />);
expect(getByTestId('upgrade-buttons')).toBeInTheDocument();
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
});

it('connectorPrompt is rendered when isAssistantEnabled is true and isWelcomeSetup is true', () => {
const props = {
...testProps,
isAssistantEnabled: true,
isWelcomeSetup: true,
};
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...props} />);
expect(getByTestId('connector-prompt')).toBeInTheDocument();
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
});

it('null is returned when isAssistantEnabled is true and isWelcomeSetup is false', () => {
const props = {
...testProps,
isAssistantEnabled: true,
isWelcomeSetup: false,
};
const { container, queryByTestId } = render(<BlockBotCallToAction {...props} />);
expect(container.firstChild).toBeNull();
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
});
});
Loading

0 comments on commit 06fabab

Please sign in to comment.