mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
# 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:
parent
ea9ec1fdb0
commit
c94b0f883b
24 changed files with 290 additions and 89 deletions
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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={[]}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>(() => {
|
||||||
|
|
|
@ -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.',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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={[]}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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={[]}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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={[]}
|
||||||
|
|
|
@ -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' });
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 s’est produite lors de l’envoi de votre message. Si le problème persiste, veuillez tester la configuration du connecteur.",
|
"xpack.elasticAssistant.assistant.apiErrorTitle": "Une erreur s’est produite lors de l’envoi 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",
|
||||||
|
|
|
@ -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": "会話セレクター",
|
||||||
|
|
|
@ -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": "对话选择器",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue