Skip to content

Commit

Permalink
[Security Solution] Adds Connector Selector to Assistant Title Header (
Browse files Browse the repository at this point in the history
…#163666)

## Summary

Adds a new `ConnectorSelectorInline` component that is displayed below
the Assistant title header.

Default:
<p align="center">
<img width="500"
src="https://github.com/elastic/kibana/assets/2946766/83e6a884-103f-43c4-9a30-a0281d9941a2"
/>
</p> 


Overflow: 
<p align="center">
<img width="500"
src="https://github.com/elastic/kibana/assets/2946766/f0d8a04e-963d-4053-90f5-2417f1c8eaca"
/>
</p> 


Missing:
<p align="center">
<img width="500"
src="https://github.com/elastic/kibana/assets/2946766/eff04e75-a5ab-468c-b801-1e056d527e6a"
/>
</p> 


Open:
<p align="center">
<img width="500"
src="https://github.com/elastic/kibana/assets/2946766/b7b97244-91a5-41ec-a096-b296e0cde644"
/>
</p> 



### Checklist

Delete any items that are not applicable to this PR.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [X] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
  • Loading branch information
spong authored Aug 16, 2023
1 parent d0231b9 commit 847e0cb
Show file tree
Hide file tree
Showing 7 changed files with 494 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ export const AssistantHeader: React.FC<Props> = ({
justifyContent={'spaceBetween'}
>
<EuiFlexItem grow={false}>
<AssistantTitle {...currentTitle} docLinks={docLinks} />
<AssistantTitle
{...currentTitle}
docLinks={docLinks}
selectedConversation={currentConversation}
/>
</EuiFlexItem>

<EuiFlexItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,29 @@ const testProps = {
title: 'Test Title',
titleIcon: 'globe',
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
selectedConversation: undefined,
};

describe('AssistantTitle', () => {
it('the component renders correctly with valid props', () => {
const { getByText, container } = render(<AssistantTitle {...testProps} />);
const { getByText, container } = render(
<TestProviders>
<AssistantTitle {...testProps} />
</TestProviders>
);
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,
});
const { getByTestId, queryByTestId } = render(
<TestProviders>
<AssistantTitle {...testProps} />
</TestProviders>,
{
wrapper: TestProviders,
}
);
expect(queryByTestId('tooltipContent')).not.toBeInTheDocument();
fireEvent.click(getByTestId('tooltipIcon'));
expect(getByTestId('tooltipContent')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import {
EuiModalHeaderTitle,
EuiPopover,
EuiText,
EuiTitle,
} from '@elastic/eui';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import * as i18n from '../translations';
import type { Conversation } from '../../..';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';

/**
* Renders a header title with an icon, a tooltip button, and a popover with
Expand All @@ -28,7 +32,10 @@ export const AssistantTitle: React.FC<{
title: string | JSX.Element;
titleIcon: string;
docLinks: Omit<DocLinksStart, 'links'>;
}> = ({ title, titleIcon, docLinks }) => {
selectedConversation: Conversation | undefined;
}> = ({ title, titleIcon, docLinks, selectedConversation }) => {
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;

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 @@ -66,32 +73,57 @@ export const AssistantTitle: React.FC<{

return (
<EuiModalHeaderTitle>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem
grow={false}
css={css`
margin-top: 3px;
`}
>
<EuiIcon data-test-subj="titleIcon" type={titleIcon} size="xl" />
</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}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="upCenter"
>
<EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}>
<h4>{i18n.TOOLTIP_TITLE}</h4>
<p>{content}</p>
</EuiText>
</EuiPopover>
</EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size={'s'}>
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
aria-label={i18n.TOOLTIP_ARIA_LABEL}
data-test-subj="tooltipIcon"
iconType="iInCircle"
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="rightUp"
>
<EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}>
<h4>{i18n.TOOLTIP_TITLE}</h4>
<EuiText size={'s'}>
<p>{content}</p>
</EuiText>
</EuiText>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ConnectorSelectorInline
isDisabled={selectedConversation === undefined}
onConnectorModalVisibilityChange={() => {}}
onConnectorSelectionChange={() => {}}
selectedConnectorId={selectedConnectorId}
selectedConversation={selectedConversation}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiModalHeaderTitle>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const ConnectorMissingCallout: React.FC<Props> = React.memo(
<p>
{' '}
<FormattedMessage
defaultMessage="Select a connector from the {link} to continue"
defaultMessage="Select a connector above or from the {link} to continue"
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
values={{
link: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 { noop } from 'lodash/fp';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { ConnectorSelectorInline } from './connector_selector_inline';
import * as i18n from '../translations';
import { Conversation } from '../../..';
import { useLoadConnectors } from '../use_load_connectors';

jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({
loadActionTypes: jest.fn(() => {
return Promise.resolve([
{
id: '.gen-ai',
name: 'Gen AI',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
]);
}),
}));

jest.mock('../use_load_connectors', () => ({
useLoadConnectors: jest.fn(() => {
return {
data: [],
error: null,
isSuccess: true,
};
}),
}));

const mockConnectors = [
{
id: 'connectorId',
name: 'Captain Connector',
isMissingSecrets: false,
actionTypeId: '.gen-ai',
config: {
apiProvider: 'OpenAI',
},
},
];

(useLoadConnectors as jest.Mock).mockReturnValue({
data: mockConnectors,
error: null,
isSuccess: true,
});

describe('ConnectorSelectorInline', () => {
it('renders empty view if no selected conversation is provided', () => {
const { getByText } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
onConnectorModalVisibilityChange={noop}
onConnectorSelectionChange={noop}
selectedConnectorId={undefined}
selectedConversation={undefined}
/>
</TestProviders>
);
expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument();
});

it('renders empty view if selectedConnectorId is NOT in list of connectors', () => {
const conversation: Conversation = {
id: 'conversation_id',
messages: [],
apiConfig: {},
};
const { getByText } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
onConnectorModalVisibilityChange={noop}
onConnectorSelectionChange={noop}
selectedConnectorId={'missing-connector-id'}
selectedConversation={conversation}
/>
</TestProviders>
);
expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument();
});

it('renders selected connector if selected selectedConnectorId is in list of connectors', () => {
const conversation: Conversation = {
id: 'conversation_id',
messages: [],
apiConfig: {},
};
const { getByText } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
onConnectorModalVisibilityChange={noop}
onConnectorSelectionChange={noop}
selectedConnectorId={mockConnectors[0].id}
selectedConversation={conversation}
/>
</TestProviders>
);
expect(getByText(mockConnectors[0].name)).toBeInTheDocument();
});
});
Loading

0 comments on commit 847e0cb

Please sign in to comment.