[8.10] [Security Solution] Fixes Assistant Connector and Actions RBAC Flow (#164382) (#164645)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Security Solution] Fixes Assistant Connector and Actions RBAC Flow
(#164382)](https://github.com/elastic/kibana/pull/164382)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Garrett
Spong","email":"spong@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-08-23T21:22:39Z","message":"[Security
Solution] Fixes Assistant Connector and Actions RBAC Flow
(#164382)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/159374 by ensuring\r\nthat if a
user doesn't have the appropriate `Connectors & Actions`\r\nprivileges,
they will be shown the appropriate messaging and any UI\r\ncontrols for
adding Connectors will be disabled or unavailable.\r\n\r\n####
Connectors and Actions `NONE` or Connectors and Actions `READ`
if\r\n*NO* existing connectors exist:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"d9535ae9-a31e-499b-9b18-6004e3db64de\"\r\n/>\r\n</p>
\r\n\r\n#### Connectors and Actions `READ` if existing connector count >
0:\r\n\r\n`Add Connector...` option isn't available:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"bd6a06a7-ffa2-4cfc-a2b7-844da99cb171\"\r\n/>\r\n</p>
\r\n\r\n<p align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"4681086e-1015-45b9-9afb-ff604c52cd38\"\r\n/>\r\n</p>
\r\n\r\n\r\n\r\nAlso addresses:\r\n\r\n* Fixes disabled state of header
connector selector for setup flows.\r\n* Adds `AssistantAvailability`
interface to `AssistantContext` for\r\nexposing ui feature controls like
`Connectors & Actions` privileges.\r\n* Hides `Add new connector...`
option if user doesn't have `ALL`\r\n`Connectors & Actions`
privileges.\r\n* Hoists dependencies from `assistant/index.tsx` to
`connector_setup` as\r\nit was already fetching dependencies from
`useAssistantContext`.\r\n\r\nNote: `ConnectorButton` and
`ConnectorMissingCallout` should probably be\r\ncombined into a single
component and show appropriate messaging given\r\nthe user's `Connectors
& Actions` privileges. I kept them separate for\r\nnow as to not modify
the control flow around the two components (till we\r\ncan further
refactor `assistant/index.tsx`), which means the missing\r\nconnector
callout is sort of doing double duty at the moment.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[X] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"db7ac1bb417a4c84d29e1d7e9e831bdaf650358c","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:
SecuritySolution","Feature:Elastic AI
Assistant","v8.10.0","v8.11.0"],"number":164382,"url":"https://github.com/elastic/kibana/pull/164382","mergeCommit":{"message":"[Security
Solution] Fixes Assistant Connector and Actions RBAC Flow
(#164382)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/159374 by ensuring\r\nthat if a
user doesn't have the appropriate `Connectors & Actions`\r\nprivileges,
they will be shown the appropriate messaging and any UI\r\ncontrols for
adding Connectors will be disabled or unavailable.\r\n\r\n####
Connectors and Actions `NONE` or Connectors and Actions `READ`
if\r\n*NO* existing connectors exist:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"d9535ae9-a31e-499b-9b18-6004e3db64de\"\r\n/>\r\n</p>
\r\n\r\n#### Connectors and Actions `READ` if existing connector count >
0:\r\n\r\n`Add Connector...` option isn't available:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"bd6a06a7-ffa2-4cfc-a2b7-844da99cb171\"\r\n/>\r\n</p>
\r\n\r\n<p align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"4681086e-1015-45b9-9afb-ff604c52cd38\"\r\n/>\r\n</p>
\r\n\r\n\r\n\r\nAlso addresses:\r\n\r\n* Fixes disabled state of header
connector selector for setup flows.\r\n* Adds `AssistantAvailability`
interface to `AssistantContext` for\r\nexposing ui feature controls like
`Connectors & Actions` privileges.\r\n* Hides `Add new connector...`
option if user doesn't have `ALL`\r\n`Connectors & Actions`
privileges.\r\n* Hoists dependencies from `assistant/index.tsx` to
`connector_setup` as\r\nit was already fetching dependencies from
`useAssistantContext`.\r\n\r\nNote: `ConnectorButton` and
`ConnectorMissingCallout` should probably be\r\ncombined into a single
component and show appropriate messaging given\r\nthe user's `Connectors
& Actions` privileges. I kept them separate for\r\nnow as to not modify
the control flow around the two components (till we\r\ncan further
refactor `assistant/index.tsx`), which means the missing\r\nconnector
callout is sort of doing double duty at the moment.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[X] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"db7ac1bb417a4c84d29e1d7e9e831bdaf650358c"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164382","number":164382,"mergeCommit":{"message":"[Security
Solution] Fixes Assistant Connector and Actions RBAC Flow
(#164382)\n\n## Summary\r\n\r\nResolves
https://github.com/elastic/kibana/issues/159374 by ensuring\r\nthat if a
user doesn't have the appropriate `Connectors & Actions`\r\nprivileges,
they will be shown the appropriate messaging and any UI\r\ncontrols for
adding Connectors will be disabled or unavailable.\r\n\r\n####
Connectors and Actions `NONE` or Connectors and Actions `READ`
if\r\n*NO* existing connectors exist:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"d9535ae9-a31e-499b-9b18-6004e3db64de\"\r\n/>\r\n</p>
\r\n\r\n#### Connectors and Actions `READ` if existing connector count >
0:\r\n\r\n`Add Connector...` option isn't available:\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"bd6a06a7-ffa2-4cfc-a2b7-844da99cb171\"\r\n/>\r\n</p>
\r\n\r\n<p align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"4681086e-1015-45b9-9afb-ff604c52cd38\"\r\n/>\r\n</p>
\r\n\r\n\r\n\r\nAlso addresses:\r\n\r\n* Fixes disabled state of header
connector selector for setup flows.\r\n* Adds `AssistantAvailability`
interface to `AssistantContext` for\r\nexposing ui feature controls like
`Connectors & Actions` privileges.\r\n* Hides `Add new connector...`
option if user doesn't have `ALL`\r\n`Connectors & Actions`
privileges.\r\n* Hoists dependencies from `assistant/index.tsx` to
`connector_setup` as\r\nit was already fetching dependencies from
`useAssistantContext`.\r\n\r\nNote: `ConnectorButton` and
`ConnectorMissingCallout` should probably be\r\ncombined into a single
component and show appropriate messaging given\r\nthe user's `Connectors
& Actions` privileges. I kept them separate for\r\nnow as to not modify
the control flow around the two components (till we\r\ncan further
refactor `assistant/index.tsx`), which means the missing\r\nconnector
callout is sort of doing double duty at the moment.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[X] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"db7ac1bb417a4c84d29e1d7e9e831bdaf650358c"}}]}]
BACKPORT-->

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-08-23 19:13:37 -04:00 committed by GitHub
parent ea9ec1fdb0
commit c94b0f883b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 290 additions and 89 deletions

View file

@ -82,6 +82,7 @@ export const AssistantHeader: React.FC<Props> = ({
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<AssistantTitle <AssistantTitle
{...currentTitle} {...currentTitle}
isDisabled={isDisabled}
docLinks={docLinks} docLinks={docLinks}
selectedConversation={currentConversation} selectedConversation={currentConversation}
/> />

View file

@ -29,11 +29,12 @@ import { ConnectorSelectorInline } from '../../connectorland/connector_selector_
* information about the assistant feature and access to documentation. * information about the assistant feature and access to documentation.
*/ */
export const AssistantTitle: React.FC<{ export const AssistantTitle: React.FC<{
isDisabled?: boolean;
title: string | JSX.Element; title: string | JSX.Element;
titleIcon: string; titleIcon: string;
docLinks: Omit<DocLinksStart, 'links'>; docLinks: Omit<DocLinksStart, 'links'>;
selectedConversation: Conversation | undefined; selectedConversation: Conversation | undefined;
}> = ({ title, titleIcon, docLinks, selectedConversation }) => { }> = ({ isDisabled = false, title, titleIcon, docLinks, selectedConversation }) => {
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId; const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
@ -116,7 +117,7 @@ export const AssistantTitle: React.FC<{
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<ConnectorSelectorInline <ConnectorSelectorInline
isDisabled={selectedConversation === undefined} isDisabled={isDisabled || selectedConversation === undefined}
onConnectorModalVisibilityChange={() => {}} onConnectorModalVisibilityChange={() => {}}
onConnectorSelectionChange={() => {}} onConnectorSelectionChange={() => {}}
selectedConnectorId={selectedConnectorId} selectedConnectorId={selectedConnectorId}

View file

@ -71,7 +71,6 @@ const AssistantComponent: React.FC<Props> = ({
setConversationId, setConversationId,
}) => { }) => {
const { const {
actionTypeRegistry,
assistantTelemetry, assistantTelemetry,
augmentMessageCodeBlocks, augmentMessageCodeBlocks,
conversations, conversations,
@ -98,11 +97,7 @@ const AssistantComponent: React.FC<Props> = ({
const { createConversation } = useConversation(); const { createConversation } = useConversation();
// Connector details // Connector details
const { const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({ http });
data: connectors,
isSuccess: areConnectorsFetched,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const defaultConnectorId = useMemo(() => getDefaultConnector(connectors)?.id, [connectors]); const defaultConnectorId = useMemo(() => getDefaultConnector(connectors)?.id, [connectors]);
const defaultProvider = useMemo( const defaultProvider = useMemo(
() => () =>
@ -171,14 +166,10 @@ const AssistantComponent: React.FC<Props> = ({
}, [areConnectorsFetched, connectors?.length, currentConversation, setLastConversationId]); }, [areConnectorsFetched, connectors?.length, currentConversation, setLastConversationId]);
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({ const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
actionTypeRegistry, conversation: blockBotConversation,
http,
refetchConnectors,
onSetupComplete: () => { onSetupComplete: () => {
bottomRef.current?.scrollIntoView({ behavior: 'auto' }); bottomRef.current?.scrollIntoView({ behavior: 'auto' });
}, },
conversation: blockBotConversation,
isConnectorConfigured: !!connectors?.length,
}); });
const currentTitle: { title: string | JSX.Element; titleIcon: string } = const currentTitle: { title: string | JSX.Element; titleIcon: string } =
@ -475,6 +466,7 @@ const AssistantComponent: React.FC<Props> = ({
<EuiFlexGroup justifyContent="spaceAround"> <EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<ConnectorMissingCallout <ConnectorMissingCallout
isConnectorConfigured={connectors?.length > 0}
isSettingsModalVisible={isSettingsModalVisible} isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible} setIsSettingsModalVisible={setIsSettingsModalVisible}
/> />

View file

@ -11,15 +11,23 @@ import React from 'react';
import { AssistantProvider, useAssistantContext } from '.'; import { AssistantProvider, useAssistantContext } from '.';
import { httpServiceMock } from '@kbn/core-http-browser-mocks'; import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import { AssistantAvailability } from '../..';
const actionTypeRegistry = actionTypeRegistryMock.create(); const actionTypeRegistry = actionTypeRegistryMock.create();
const mockGetInitialConversations = jest.fn(() => ({})); const mockGetInitialConversations = jest.fn(() => ({}));
const mockGetComments = jest.fn(() => []); const mockGetComments = jest.fn(() => []);
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
const ContextWrapper: React.FC = ({ children }) => ( const ContextWrapper: React.FC = ({ children }) => (
<AssistantProvider <AssistantProvider
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn()} augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]} baseAllow={[]}
baseAllowReplacement={[]} baseAllowReplacement={[]}

View file

@ -33,7 +33,7 @@ import {
SYSTEM_PROMPT_LOCAL_STORAGE_KEY, SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
} from './constants'; } from './constants';
import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings'; import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings';
import { AssistantTelemetry } from './types'; import { AssistantAvailability, AssistantTelemetry } from './types';
export interface ShowAssistantOverlayProps { export interface ShowAssistantOverlayProps {
showOverlay: boolean; showOverlay: boolean;
@ -48,6 +48,7 @@ type ShowAssistantOverlay = ({
}: ShowAssistantOverlayProps) => void; }: ShowAssistantOverlayProps) => void;
export interface AssistantProviderProps { export interface AssistantProviderProps {
actionTypeRegistry: ActionTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantTelemetry?: AssistantTelemetry; assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][]; augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
baseAllow: string[]; baseAllow: string[];
@ -79,6 +80,7 @@ export interface AssistantProviderProps {
export interface UseAssistantContext { export interface UseAssistantContext {
actionTypeRegistry: ActionTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantTelemetry?: AssistantTelemetry; assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][]; augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
allQuickPrompts: QuickPrompt[]; allQuickPrompts: QuickPrompt[];
@ -126,6 +128,7 @@ const AssistantContext = React.createContext<UseAssistantContext | undefined>(un
export const AssistantProvider: React.FC<AssistantProviderProps> = ({ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
actionTypeRegistry, actionTypeRegistry,
assistantAvailability,
assistantTelemetry, assistantTelemetry,
augmentMessageCodeBlocks, augmentMessageCodeBlocks,
baseAllow, baseAllow,
@ -244,6 +247,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
const value = useMemo( const value = useMemo(
() => ({ () => ({
actionTypeRegistry, actionTypeRegistry,
assistantAvailability,
assistantTelemetry, assistantTelemetry,
augmentMessageCodeBlocks, augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [], allQuickPrompts: localStorageQuickPrompts ?? [],
@ -279,6 +283,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
}), }),
[ [
actionTypeRegistry, actionTypeRegistry,
assistantAvailability,
assistantTelemetry, assistantTelemetry,
augmentMessageCodeBlocks, augmentMessageCodeBlocks,
baseAllow, baseAllow,

View file

@ -62,3 +62,14 @@ export interface AssistantTelemetry {
reportAssistantMessageSent: (params: { conversationId: string; role: string }) => void; reportAssistantMessageSent: (params: { conversationId: string; role: string }) => void;
reportAssistantQuickPrompt: (params: { conversationId: string; promptTitle: string }) => void; reportAssistantQuickPrompt: (params: { conversationId: string; promptTitle: string }) => void;
} }
export interface AssistantAvailability {
// True when user is Enterprise, or Security Complete PLI for serverless. When false, the Assistant is disabled and unavailable
isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable
hasAssistantPrivilege: boolean;
// When true, user has `All` privilege for `Connectors and Actions` (show/execute/delete/save ui capabilities)
hasConnectorsAllPrivilege: boolean;
// When true, user has `Read` privilege for `Connectors and Actions` (show/execute ui capabilities)
hasConnectorsReadPrivilege: boolean;
}

View file

@ -5,32 +5,46 @@
* 2.0. * 2.0.
*/ */
import React from 'react'; import React, { useCallback } from 'react';
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { GenAiLogo } from '@kbn/stack-connectors-plugin/public/common'; import { GenAiLogo } from '@kbn/stack-connectors-plugin/public/common';
import * as i18n from '../translations'; import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
export interface ConnectorButtonProps { export interface ConnectorButtonProps {
setIsConnectorModalVisible: React.Dispatch<React.SetStateAction<boolean>>; setIsConnectorModalVisible?: React.Dispatch<React.SetStateAction<boolean>>;
} }
/** /**
* Simple button component for adding a connector. Note: component is basic and does not handle connector * Simple button component for adding a connector. Note: component is basic and does not handle connector
* add logic. Must pass in `setIsConnectorModalVisible`, see ConnectorSetup component if wanting to manage * add logic. See ConnectorSetup component if wanting to manage connector add logic.
* connector add logic.
*/ */
export const ConnectorButton: React.FC<ConnectorButtonProps> = React.memo<ConnectorButtonProps>( export const ConnectorButton: React.FC<ConnectorButtonProps> = React.memo<ConnectorButtonProps>(
({ setIsConnectorModalVisible }) => { ({ setIsConnectorModalVisible }) => {
const { assistantAvailability } = useAssistantContext();
const title = assistantAvailability.hasConnectorsAllPrivilege
? i18n.ADD_CONNECTOR_TITLE
: i18n.ADD_CONNECTOR_MISSING_PRIVILEGES_TITLE;
const description = assistantAvailability.hasConnectorsAllPrivilege
? i18n.ADD_CONNECTOR_DESCRIPTION
: i18n.ADD_CONNECTOR_MISSING_PRIVILEGES_DESCRIPTION;
const onClick = useCallback(() => {
setIsConnectorModalVisible?.(true);
}, [setIsConnectorModalVisible]);
return ( return (
<EuiFlexGroup gutterSize="l" justifyContent="spaceAround"> <EuiFlexGroup gutterSize="l" justifyContent="spaceAround">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiCard <EuiCard
data-test-subj="connectorButton"
layout="horizontal" layout="horizontal"
icon={<EuiIcon size="xl" type={GenAiLogo} />} icon={<EuiIcon size="xl" type={GenAiLogo} />}
title={i18n.ADD_CONNECTOR_TITLE} title={title}
description={i18n.ADD_CONNECTOR_DESCRIPTION} description={description}
onClick={() => setIsConnectorModalVisible(true)} onClick={assistantAvailability.hasConnectorsAllPrivilege ? onClick : undefined}
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -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 from 'react';
import { render } from '@testing-library/react';
import { ConnectorMissingCallout } from '.';
import { AssistantAvailability } from '../../..';
import { TestProviders } from '../../mock/test_providers/test_providers';
describe('connectorMissingCallout', () => {
describe('when connectors and actions privileges', () => {
describe('are `READ`', () => {
const assistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
it('should show connector privileges required button if no connectors exist', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={false}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).toBeInTheDocument();
});
it('should NOT show connector privileges required button if at least one connector exists', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).not.toBeInTheDocument();
});
});
describe('are `NONE`', () => {
const assistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: false,
isAssistantEnabled: true,
};
it('should show connector privileges required button', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).toBeInTheDocument();
});
});
});
});

View file

@ -12,22 +12,24 @@ import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from '../translations'; import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context'; import { useAssistantContext } from '../../assistant_context';
import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings'; import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
import { ConnectorButton } from '../connector_button';
interface Props { interface Props {
isConnectorConfigured: boolean;
isSettingsModalVisible: boolean; isSettingsModalVisible: boolean;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>; setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
} }
/** /**
* Error callout to be displayed when there is no connector configured for a conversation. Includes deep-link * Error callout to be displayed when there is no connector configured for a conversation. Includes deep-link
* to conversation settings to quickly resolve. * to conversation settings to quickly resolve. Falls back to <ConnectorButton /> connector if privileges aren't met.
* *
* TODO: Add 'quick fix' button to just pick a connector * TODO: Add 'quick fix' button to just pick a connector
* TODO: Add setting for 'default connector' so we can auto-resolve and not even show this * TODO: Add setting for 'default connector' so we can auto-resolve and not even show this
*/ */
export const ConnectorMissingCallout: React.FC<Props> = React.memo( export const ConnectorMissingCallout: React.FC<Props> = React.memo(
({ isSettingsModalVisible, setIsSettingsModalVisible }) => { ({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible }) => {
const { setSelectedSettingsTab } = useAssistantContext(); const { assistantAvailability, setSelectedSettingsTab } = useAssistantContext();
const onConversationSettingsClicked = useCallback(() => { const onConversationSettingsClicked = useCallback(() => {
if (!isSettingsModalVisible) { if (!isSettingsModalVisible) {
@ -36,28 +38,40 @@ export const ConnectorMissingCallout: React.FC<Props> = React.memo(
} }
}, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]); }, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]);
// Show missing callout if user has all privileges or read privileges and at least 1 connector configured
const showMissingCallout =
assistantAvailability.hasConnectorsAllPrivilege ||
(assistantAvailability.hasConnectorsReadPrivilege && isConnectorConfigured);
return ( return (
<EuiCallOut <>
color="danger" {showMissingCallout ? (
iconType="controlsVertical" <EuiCallOut
size="m" data-test-subj="connectorMissingCallout"
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE} color="danger"
> iconType="controlsVertical"
<p> size="m"
{' '} title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
<FormattedMessage >
defaultMessage="Select a connector above or from the {link} to continue" <p>
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription" {' '}
values={{ <FormattedMessage
link: ( defaultMessage="Select a connector above or from the {link} to continue"
<EuiLink onClick={onConversationSettingsClicked}> id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK} values={{
</EuiLink> link: (
), <EuiLink onClick={onConversationSettingsClicked}>
}} {i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
/> </EuiLink>
</p> ),
</EuiCallOut> }}
/>
</p>
</EuiCallOut>
) : (
<ConnectorButton />
)}
</>
); );
} }
); );

View file

@ -23,6 +23,7 @@ import {
import { useLoadConnectors } from '../use_load_connectors'; import { useLoadConnectors } from '../use_load_connectors';
import * as i18n from '../translations'; import * as i18n from '../translations';
import { useLoadActionTypes } from '../use_load_action_types'; import { useLoadActionTypes } from '../use_load_action_types';
import { useAssistantContext } from '../../assistant_context';
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR'; export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
interface Props { interface Props {
@ -47,6 +48,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
selectedConnectorId, selectedConnectorId,
onConnectorSelectionChange, onConnectorSelectionChange,
}) => { }) => {
const { assistantAvailability } = useAssistantContext();
// Connector Modal State // Connector Modal State
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false); const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
const { data: actionTypes } = useLoadActionTypes({ http }); const { data: actionTypes } = useLoadActionTypes({ http });
@ -68,6 +70,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
refetch: refetchConnectors, refetch: refetchConnectors,
} = useLoadConnectors({ http }); } = useLoadConnectors({ http });
const isLoading = isLoadingActionTypes || isFetchingActionTypes; const isLoading = isLoadingActionTypes || isFetchingActionTypes;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const addNewConnectorOption = useMemo(() => { const addNewConnectorOption = useMemo(() => {
return { return {
@ -113,6 +116,15 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
); );
}, [connectors]); }, [connectors]);
// Only include add new connector option if user has privilege
const allConnectorOptions = useMemo(
() =>
assistantAvailability.hasConnectorsAllPrivilege
? [...connectorOptions, addNewConnectorOption]
: [...connectorOptions],
[addNewConnectorOption, assistantAvailability.hasConnectorsAllPrivilege, connectorOptions]
);
const cleanupAndCloseModal = useCallback(() => { const cleanupAndCloseModal = useCallback(() => {
onConnectorModalVisibilityChange?.(false); onConnectorModalVisibilityChange?.(false);
setIsConnectorModalVisible(false); setIsConnectorModalVisible(false);
@ -139,11 +151,11 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<EuiSuperSelect <EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE} aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true} compressed={true}
disabled={isDisabled} disabled={localIsDisabled}
hasDividers={true} hasDividers={true}
isLoading={isLoading} isLoading={isLoading}
onChange={onChange} onChange={onChange}
options={[...connectorOptions, addNewConnectorOption]} options={allConnectorOptions}
valueOfSelected={selectedConnectorId ?? ''} valueOfSelected={selectedConnectorId ?? ''}
/> />
{isConnectorModalVisible && ( {isConnectorModalVisible && (

View file

@ -85,7 +85,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
onConnectorSelectionChange, onConnectorSelectionChange,
}) => { }) => {
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const { actionTypeRegistry, http } = useAssistantContext(); const { actionTypeRegistry, assistantAvailability, http } = useAssistantContext();
const { setApiConfig } = useConversation(); const { setApiConfig } = useConversation();
// Connector Modal State // Connector Modal State
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false); const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
@ -111,6 +111,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
const selectedConnectorName = const selectedConnectorName =
connectors?.find((c) => c.id === selectedConnectorId)?.name ?? connectors?.find((c) => c.id === selectedConnectorId)?.name ??
i18n.INLINE_CONNECTOR_PLACEHOLDER; i18n.INLINE_CONNECTOR_PLACEHOLDER;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const addNewConnectorOption = useMemo(() => { const addNewConnectorOption = useMemo(() => {
return { return {
@ -160,6 +161,15 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
); );
}, [connectors]); }, [connectors]);
// Only include add new connector option if user has privilege
const allConnectorOptions = useMemo(
() =>
assistantAvailability.hasConnectorsAllPrivilege
? [...connectorOptions, addNewConnectorOption]
: [...connectorOptions],
[addNewConnectorOption, assistantAvailability.hasConnectorsAllPrivilege, connectorOptions]
);
const cleanupAndCloseModal = useCallback(() => { const cleanupAndCloseModal = useCallback(() => {
onConnectorModalVisibilityChange?.(false); onConnectorModalVisibilityChange?.(false);
setIsConnectorModalVisible(false); setIsConnectorModalVisible(false);
@ -236,13 +246,13 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
<EuiSuperSelect <EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE} aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true} compressed={true}
disabled={isDisabled} disabled={localIsDisabled}
hasDividers={true} hasDividers={true}
isLoading={isLoading} isLoading={isLoading}
isOpen={isOpen} isOpen={isOpen}
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={onChange} onChange={onChange}
options={[...connectorOptions, addNewConnectorOption]} options={allConnectorOptions}
placeholder={placeholderComponent} placeholder={placeholderComponent}
valueOfSelected={selectedConnectorId} valueOfSelected={selectedConnectorId}
/> />
@ -254,7 +264,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
data-test-subj="connectorSelectorPlaceholderButton" data-test-subj="connectorSelectorPlaceholderButton"
iconSide={'right'} iconSide={'right'}
iconType="arrowDown" iconType="arrowDown"
isDisabled={isDisabled} isDisabled={localIsDisabled}
onClick={onConnectorClick} onClick={onConnectorClick}
size="xs" size="xs"
> >

View file

@ -13,8 +13,7 @@ import styled from 'styled-components';
import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
import { HttpSetup } from '@kbn/core-http-browser'; import { ActionType } from '@kbn/triggers-actions-ui-plugin/public';
import { ActionType, ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { import {
GEN_AI_CONNECTOR_ID, GEN_AI_CONNECTOR_ID,
OpenAiProviderType, OpenAiProviderType,
@ -29,6 +28,7 @@ import { useConversation } from '../../assistant/use_conversation';
import { clearPresentationData, conversationHasNoPresentationData } from './helpers'; import { clearPresentationData, conversationHasNoPresentationData } from './helpers';
import * as i18n from '../translations'; import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context'; import { useAssistantContext } from '../../assistant_context';
import { useLoadConnectors } from '../use_load_connectors';
const ConnectorButtonWrapper = styled.div` const ConnectorButtonWrapper = styled.div`
margin-bottom: 10px; margin-bottom: 10px;
@ -43,21 +43,13 @@ interface Config {
} }
export interface ConnectorSetupProps { export interface ConnectorSetupProps {
isConnectorConfigured: boolean;
actionTypeRegistry: ActionTypeRegistryContract;
conversation?: Conversation; conversation?: Conversation;
http: HttpSetup;
onSetupComplete?: () => void; onSetupComplete?: () => void;
refetchConnectors?: () => void;
} }
export const useConnectorSetup = ({ export const useConnectorSetup = ({
actionTypeRegistry,
conversation = WELCOME_CONVERSATION, conversation = WELCOME_CONVERSATION,
http,
isConnectorConfigured = false,
onSetupComplete, onSetupComplete,
refetchConnectors,
}: ConnectorSetupProps): { }: ConnectorSetupProps): {
comments: EuiCommentProps[]; comments: EuiCommentProps[];
prompt: React.ReactElement; prompt: React.ReactElement;
@ -65,7 +57,13 @@ export const useConnectorSetup = ({
const { appendMessage, setApiConfig, setConversation } = useConversation(); const { appendMessage, setApiConfig, setConversation } = useConversation();
const bottomRef = useRef<HTMLDivElement | null>(null); const bottomRef = useRef<HTMLDivElement | null>(null);
// Access all conversations so we can add connector to all on initial setup // Access all conversations so we can add connector to all on initial setup
const { conversations } = useAssistantContext(); const { actionTypeRegistry, conversations, http } = useAssistantContext();
const {
data: connectors,
isSuccess: areConnectorsFetched,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const isConnectorConfigured = areConnectorsFetched && !!connectors?.length;
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false); const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
const [showAddConnectorButton, setShowAddConnectorButton] = useState<boolean>(() => { const [showAddConnectorButton, setShowAddConnectorButton] = useState<boolean>(() => {

View file

@ -73,17 +73,17 @@ export const ADD_CONNECTOR_DESCRIPTION = i18n.translate(
} }
); );
export const CONNECTOR_ADDED_TITLE = i18n.translate( export const ADD_CONNECTOR_MISSING_PRIVILEGES_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle', 'xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesTitle',
{ {
defaultMessage: 'Generative AI Connector added!', defaultMessage: 'Generative AI Connector Required',
} }
); );
export const CONNECTOR_ADDED_DESCRIPTION = i18n.translate( export const ADD_CONNECTOR_MISSING_PRIVILEGES_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription', 'xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesDescription',
{ {
defaultMessage: 'Ready to continue the conversation...', defaultMessage: 'Please contact your administrator to enable a Generative AI Connector.',
} }
); );

View file

@ -15,9 +15,10 @@ import { ThemeProvider } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AssistantProvider, AssistantProviderProps } from '../../assistant_context'; import { AssistantProvider, AssistantProviderProps } from '../../assistant_context';
import { Conversation } from '../../assistant_context/types'; import { AssistantAvailability, Conversation } from '../../assistant_context/types';
interface Props { interface Props {
assistantAvailability?: AssistantAvailability;
children: React.ReactNode; children: React.ReactNode;
getInitialConversations?: () => Record<string, Conversation>; getInitialConversations?: () => Record<string, Conversation>;
providerContext?: Partial<AssistantProviderProps>; providerContext?: Partial<AssistantProviderProps>;
@ -28,8 +29,16 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
const mockGetInitialConversations = () => ({}); const mockGetInitialConversations = () => ({});
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
/** A utility for wrapping children in the providers required to run tests */ /** A utility for wrapping children in the providers required to run tests */
export const TestProvidersComponent: React.FC<Props> = ({ export const TestProvidersComponent: React.FC<Props> = ({
assistantAvailability = mockAssistantAvailability,
children, children,
getInitialConversations = mockGetInitialConversations, getInitialConversations = mockGetInitialConversations,
providerContext, providerContext,
@ -56,6 +65,7 @@ export const TestProvidersComponent: React.FC<Props> = ({
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AssistantProvider <AssistantProvider
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
assistantAvailability={assistantAvailability}
augmentMessageCodeBlocks={jest.fn().mockReturnValue([])} augmentMessageCodeBlocks={jest.fn().mockReturnValue([])}
baseAllow={[]} baseAllow={[]}
baseAllowReplacement={[]} baseAllowReplacement={[]}

View file

@ -89,8 +89,16 @@ export type {
QueryType, QueryType,
} from './impl/assistant/use_conversation/helpers'; } from './impl/assistant/use_conversation/helpers';
/** serialized conversations */ export type {
export type { AssistantTelemetry, Conversation, Message } from './impl/assistant_context/types'; /** Feature Availability Interface */
AssistantAvailability,
/** Telemetry Interface */
AssistantTelemetry,
/** Conversation Interface */
Conversation,
/** Message Interface */
Message,
} from './impl/assistant_context/types';
/** Interface for defining system/user prompts */ /** Interface for defining system/user prompts */
export type { Prompt } from './impl/assistant/types'; export type { Prompt } from './impl/assistant/types';

View file

@ -7,7 +7,7 @@
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import { httpServiceMock } from '@kbn/core-http-browser-mocks'; import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { AssistantProvider } from '@kbn/elastic-assistant'; import { AssistantAvailability, AssistantProvider } from '@kbn/elastic-assistant';
import { I18nProvider } from '@kbn/i18n-react'; import { I18nProvider } from '@kbn/i18n-react';
import { euiDarkVars } from '@kbn/ui-theme'; import { euiDarkVars } from '@kbn/ui-theme';
import React from 'react'; import React from 'react';
@ -32,11 +32,19 @@ export const TestProvidersComponent: React.FC<Props> = ({ children }) => {
reportDataQualityIndexChecked: jest.fn(), reportDataQualityIndexChecked: jest.fn(),
reportDataQualityCheckAllCompleted: jest.fn(), reportDataQualityCheckAllCompleted: jest.fn(),
}; };
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
return ( return (
<I18nProvider> <I18nProvider>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<AssistantProvider <AssistantProvider
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn()} augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]} baseAllow={[]}
baseAllowReplacement={[]} baseAllowReplacement={[]}

View file

@ -42,6 +42,7 @@ import { PROMPT_CONTEXTS } from '../assistant/content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from '../assistant/content/quick_prompts'; import { BASE_SECURITY_QUICK_PROMPTS } from '../assistant/content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from '../assistant/content/prompts/system'; import { BASE_SECURITY_SYSTEM_PROMPTS } from '../assistant/content/prompts/system';
import { useAnonymizationStore } from '../assistant/use_anonymization_store'; import { useAnonymizationStore } from '../assistant/use_anonymization_store';
import { useAssistantAvailability } from '../assistant/use_assistant_availability';
interface StartAppComponent { interface StartAppComponent {
children: React.ReactNode; children: React.ReactNode;
@ -70,6 +71,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
upselling, upselling,
} = services; } = services;
const assistantAvailability = useAssistantAvailability();
const { conversations, setConversations } = useConversationStore(); const { conversations, setConversations } = useConversationStore();
const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } = const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } =
useAnonymizationStore(); useAnonymizationStore();
@ -96,6 +98,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
<AssistantProvider <AssistantProvider
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks} augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
assistantTelemetry={assistantTelemetry} assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow} defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement} defaultAllowReplacement={defaultAllowReplacement}

View file

@ -14,15 +14,31 @@ export interface UseAssistantAvailability {
isAssistantEnabled: boolean; isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable // When true, the Assistant is hidden and unavailable
hasAssistantPrivilege: boolean; hasAssistantPrivilege: boolean;
// When true, user has `All` privilege for `Connectors and Actions` (show/execute/delete/save ui capabilities)
hasConnectorsAllPrivilege: boolean;
// When true, user has `Read` privilege for `Connectors and Actions` (show/execute ui capabilities)
hasConnectorsReadPrivilege: boolean;
} }
export const useAssistantAvailability = (): UseAssistantAvailability => { export const useAssistantAvailability = (): UseAssistantAvailability => {
const isEnterprise = useLicense().isEnterprise(); const isEnterprise = useLicense().isEnterprise();
const capabilities = useKibana().services.application.capabilities; const capabilities = useKibana().services.application.capabilities;
const isAssistantEnabled = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true;
// Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts
// `READ` ui capabilities defined as: { ui: ['show', 'execute'] }
const hasConnectorsReadPrivilege =
capabilities.actions?.show === true && capabilities.actions?.execute === true;
// `ALL` ui capabilities defined as: { ui: ['show', 'execute', 'save', 'delete'] }
const hasConnectorsAllPrivilege =
hasConnectorsReadPrivilege &&
capabilities.actions?.delete === true &&
capabilities.actions?.save === true;
return { return {
hasAssistantPrivilege,
hasConnectorsAllPrivilege,
hasConnectorsReadPrivilege,
isAssistantEnabled: isEnterprise, isAssistantEnabled: isEnterprise,
hasAssistantPrivilege: isAssistantEnabled,
}; };
}; };

View file

@ -8,6 +8,7 @@
import { httpServiceMock } from '@kbn/core-http-browser-mocks'; import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import React from 'react'; import React from 'react';
import type { AssistantAvailability } from '@kbn/elastic-assistant';
import { AssistantProvider } from '@kbn/elastic-assistant'; import { AssistantProvider } from '@kbn/elastic-assistant';
interface Props { interface Props {
@ -22,10 +23,17 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({ children }) =>
const actionTypeRegistry = actionTypeRegistryMock.create(); const actionTypeRegistry = actionTypeRegistryMock.create();
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
mockHttp.get.mockResolvedValue([]); mockHttp.get.mockResolvedValue([]);
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
return ( return (
<AssistantProvider <AssistantProvider
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn(() => [])} augmentMessageCodeBlocks={jest.fn(() => [])}
baseAllow={[]} baseAllow={[]}
baseAllowReplacement={[]} baseAllowReplacement={[]}

View file

@ -28,9 +28,12 @@ describe('useAssistant', () => {
let hookResult: RenderHookResult<UseAssistantParams, UseAssistantResult>; let hookResult: RenderHookResult<UseAssistantParams, UseAssistantResult>;
it(`should return showAssistant true and a value for promptContextId`, () => { it(`should return showAssistant true and a value for promptContextId`, () => {
jest jest.mocked(useAssistantAvailability).mockReturnValue({
.mocked(useAssistantAvailability) hasAssistantPrivilege: true,
.mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true }); hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest jest
.mocked(useAssistantOverlay) .mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });
@ -42,9 +45,12 @@ describe('useAssistant', () => {
}); });
it(`should return showAssistant false if hasAssistantPrivilege is false`, () => { it(`should return showAssistant false if hasAssistantPrivilege is false`, () => {
jest jest.mocked(useAssistantAvailability).mockReturnValue({
.mocked(useAssistantAvailability) hasAssistantPrivilege: false,
.mockReturnValue({ hasAssistantPrivilege: false, isAssistantEnabled: true }); hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest jest
.mocked(useAssistantOverlay) .mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });
@ -56,9 +62,12 @@ describe('useAssistant', () => {
}); });
it('returns anonymized prompt context data', async () => { it('returns anonymized prompt context data', async () => {
jest jest.mocked(useAssistantAvailability).mockReturnValue({
.mocked(useAssistantAvailability) hasAssistantPrivilege: true,
.mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true }); hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest jest
.mocked(useAssistantOverlay) .mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });

View file

@ -114,6 +114,8 @@ describe('Details Panel Component', () => {
describe('DetailsPanel: rendering', () => { describe('DetailsPanel: rendering', () => {
beforeEach(() => { beforeEach(() => {
(useAssistantAvailability as jest.Mock).mockReturnValue({ (useAssistantAvailability as jest.Mock).mockReturnValue({
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
hasAssistantPrivilege: false, hasAssistantPrivilege: false,
isAssistantEnabled: true, isAssistantEnabled: true,
}); });

View file

@ -11938,8 +11938,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "{total} champs dans ce contexte sont disponibles pour être inclus dans la conversation", "xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "{total} champs dans ce contexte sont disponibles pour être inclus dans la conversation",
"xpack.elasticAssistant.assistant.apiErrorTitle": "Une erreur sest produite lors de lenvoi de votre message. Si le problème persiste, veuillez tester la configuration du connecteur.", "xpack.elasticAssistant.assistant.apiErrorTitle": "Une erreur sest produite lors de lenvoi de votre message. Si le problème persiste, veuillez tester la configuration du connecteur.",
"xpack.elasticAssistant.assistant.clearChat": "Effacer le chat", "xpack.elasticAssistant.assistant.clearChat": "Effacer le chat",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "Prêt à continuer la conversation...",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "Connecteur d'intelligence artificielle générative ajouté.",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "Configurez un connecteur pour continuer la conversation", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "Configurez un connecteur pour continuer la conversation",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "Ajouter un connecteur d'intelligence artificielle générative", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "Ajouter un connecteur d'intelligence artificielle générative",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "Sélecteur de conversation", "xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "Sélecteur de conversation",

View file

@ -11952,8 +11952,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "このコンテキストの{total}個のフィールドを会話に含めることができます", "xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "このコンテキストの{total}個のフィールドを会話に含めることができます",
"xpack.elasticAssistant.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。問題が解決しない場合は、コネクター構成をテストしてください。", "xpack.elasticAssistant.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。問題が解決しない場合は、コネクター構成をテストしてください。",
"xpack.elasticAssistant.assistant.clearChat": "チャットを消去", "xpack.elasticAssistant.assistant.clearChat": "チャットを消去",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "会話を続行する準備ができました...",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "生成AIコネクターが追加されました。",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "会話を続行するようにコネクターを構成", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "会話を続行するようにコネクターを構成",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "生成AIコネクターを追加", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "生成AIコネクターを追加",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "会話セレクター", "xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "会話セレクター",

View file

@ -11952,8 +11952,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "可在对话中包含此上下文中的 {total} 个字段", "xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "可在对话中包含此上下文中的 {total} 个字段",
"xpack.elasticAssistant.assistant.apiErrorTitle": "发送消息时出错。如果问题持续存在,请测试连接器配置。", "xpack.elasticAssistant.assistant.apiErrorTitle": "发送消息时出错。如果问题持续存在,请测试连接器配置。",
"xpack.elasticAssistant.assistant.clearChat": "清除聊天", "xpack.elasticAssistant.assistant.clearChat": "清除聊天",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "准备继续对话……",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "已添加生成式 AI 连接器!",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "配置连接器以继续对话", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "配置连接器以继续对话",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "添加生成式 AI 连接器", "xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "添加生成式 AI 连接器",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "对话选择器", "xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "对话选择器",