mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Connector selector onboarding (#203742)
## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. https://github.com/user-attachments/assets/6d7527d1-dc8d-4f3a-9b03-cfd0022701d2 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e394abf6e9
commit
86666bf790
26 changed files with 740 additions and 304 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -965,6 +965,7 @@ x-pack/solutions/search/plugins/search_playground @elastic/search-kibana
|
|||
x-pack/solutions/search/plugins/search_solution/search_navigation @elastic/search-kibana
|
||||
x-pack/solutions/search/plugins/search_synonyms @elastic/search-kibana
|
||||
x-pack/solutions/search/plugins/serverless_search @elastic/search-kibana
|
||||
x-pack/solutions/security/packages/connectors @elastic/security-threat-hunting-explore
|
||||
x-pack/solutions/security/packages/data_table @elastic/security-threat-hunting-investigations
|
||||
x-pack/solutions/security/packages/data-stream-adapter @elastic/security-threat-hunting
|
||||
x-pack/solutions/security/packages/distribution_bar @elastic/kibana-cloud-security-posture
|
||||
|
|
|
@ -822,6 +822,7 @@
|
|||
"@kbn/security-plugin-types-public": "link:x-pack/platform/packages/shared/security/plugin_types_public",
|
||||
"@kbn/security-plugin-types-server": "link:x-pack/platform/packages/shared/security/plugin_types_server",
|
||||
"@kbn/security-role-management-model": "link:x-pack/platform/packages/private/security/role_management_model",
|
||||
"@kbn/security-solution-connectors": "link:x-pack/solutions/security/packages/connectors",
|
||||
"@kbn/security-solution-distribution-bar": "link:x-pack/solutions/security/packages/distribution_bar",
|
||||
"@kbn/security-solution-ess": "link:x-pack/solutions/security/plugins/security_solution_ess",
|
||||
"@kbn/security-solution-features": "link:x-pack/solutions/security/packages/features",
|
||||
|
|
|
@ -1632,6 +1632,8 @@
|
|||
"@kbn/security-plugin-types-server/*": ["x-pack/platform/packages/shared/security/plugin_types_server/*"],
|
||||
"@kbn/security-role-management-model": ["x-pack/platform/packages/private/security/role_management_model"],
|
||||
"@kbn/security-role-management-model/*": ["x-pack/platform/packages/private/security/role_management_model/*"],
|
||||
"@kbn/security-solution-connectors": ["x-pack/solutions/security/packages/connectors"],
|
||||
"@kbn/security-solution-connectors/*": ["x-pack/solutions/security/packages/connectors/*"],
|
||||
"@kbn/security-solution-distribution-bar": ["x-pack/solutions/security/packages/distribution_bar"],
|
||||
"@kbn/security-solution-distribution-bar/*": ["x-pack/solutions/security/packages/distribution_bar/*"],
|
||||
"@kbn/security-solution-ess": ["x-pack/solutions/security/plugins/security_solution_ess"],
|
||||
|
|
9
x-pack/solutions/security/packages/connectors/index.ts
Normal file
9
x-pack/solutions/security/packages/connectors/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ConnectorSelector } from './src/connector_selector';
|
||||
export type { ConnectorSelectorProps } from './src/connector_selector';
|
12
x-pack/solutions/security/packages/connectors/jest.config.js
Normal file
12
x-pack/solutions/security/packages/connectors/jest.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
roots: ['<rootDir>/x-pack/solutions/security/packages/connectors'],
|
||||
rootDir: '../../../../..',
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/security-solution-connectors",
|
||||
"owner": [
|
||||
"@elastic/security-threat-hunting-explore"
|
||||
],
|
||||
"group": "security",
|
||||
"visibility": "private"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/security-solution-connectors",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
export const useConnectorSelectorStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return {
|
||||
placeholder: css`
|
||||
color: ${euiTheme.colors.primary};
|
||||
margin-right: ${euiTheme.size.xs};
|
||||
`,
|
||||
optionDisplay: css`
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
offset: css`
|
||||
width: 24px;
|
||||
`,
|
||||
inputContainer: css`
|
||||
.euiSuperSelectControl {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.euiFormControlLayoutIcons {
|
||||
right: 14px;
|
||||
top: 2px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { some } from 'lodash';
|
||||
import * as i18n from './translations';
|
||||
import { useConnectorSelectorStyles } from './connector_selector.styles';
|
||||
import { ADD_NEW_CONNECTOR } from './constants';
|
||||
|
||||
export interface ConnectorDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ConnectorSelectorProps {
|
||||
connectors: ConnectorDetails[];
|
||||
onChange: (connectorId: string) => void;
|
||||
selectedId?: string;
|
||||
onNewConnectorClicked?: () => void;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
||||
({ connectors, onChange, selectedId, onNewConnectorClicked, isDisabled }) => {
|
||||
const styles = useConnectorSelectorStyles();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const addNewConnectorOption = useMemo(() => {
|
||||
return {
|
||||
value: ADD_NEW_CONNECTOR,
|
||||
inputDisplay: i18n.ADD_NEW_CONNECTOR,
|
||||
dropdownDisplay: (
|
||||
<EuiFlexGroup gutterSize="none" key={ADD_NEW_CONNECTOR}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="addNewConnectorButton"
|
||||
href="#"
|
||||
isDisabled={isDisabled}
|
||||
iconType="plus"
|
||||
size="xs"
|
||||
>
|
||||
{i18n.ADD_NEW_CONNECTOR}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{/* Right offset to compensate for 'selected' icon of EuiSuperSelect since native footers aren't supported*/}
|
||||
<div css={styles.offset} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
}, [isDisabled, styles.offset]);
|
||||
|
||||
const connectorExists = useMemo(
|
||||
() => some(connectors, ['id', selectedId]),
|
||||
[connectors, selectedId]
|
||||
);
|
||||
|
||||
const mappedConnectorOptions = connectors.map((connector) => ({
|
||||
value: connector.id,
|
||||
'data-test-subj': connector.id,
|
||||
inputDisplay: (
|
||||
<EuiText css={styles.optionDisplay} size="s" color={euiTheme.colors.primary}>
|
||||
{connector.name}
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<React.Fragment key={connector.id}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false} data-test-subj={`connector-${connector.name}`}>
|
||||
<strong>{connector.name}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{connector.description}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</React.Fragment>
|
||||
),
|
||||
}));
|
||||
|
||||
const allConnectorOptions = useMemo(
|
||||
() =>
|
||||
onNewConnectorClicked
|
||||
? [...mappedConnectorOptions, addNewConnectorOption]
|
||||
: [...mappedConnectorOptions],
|
||||
[onNewConnectorClicked, mappedConnectorOptions, addNewConnectorOption]
|
||||
);
|
||||
|
||||
const onChangeConnector = useCallback(
|
||||
(connectorId: string) => {
|
||||
if (connectorId === ADD_NEW_CONNECTOR) {
|
||||
onNewConnectorClicked?.();
|
||||
return;
|
||||
}
|
||||
onChange(connectorId);
|
||||
},
|
||||
[onChange, onNewConnectorClicked]
|
||||
);
|
||||
|
||||
return (
|
||||
<div css={styles.inputContainer}>
|
||||
{!connectorExists && !connectors.length ? (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="addNewConnectorButton"
|
||||
iconType="plusInCircle"
|
||||
isDisabled={isDisabled}
|
||||
size="xs"
|
||||
onClick={() => onNewConnectorClicked?.()}
|
||||
>
|
||||
{i18n.ADD_CONNECTOR}
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiSuperSelect
|
||||
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
|
||||
css={styles.placeholder}
|
||||
compressed={true}
|
||||
data-test-subj="connector-selector"
|
||||
disabled={isDisabled}
|
||||
hasDividers={true}
|
||||
onChange={onChangeConnector}
|
||||
options={allConnectorOptions}
|
||||
valueOfSelected={selectedId}
|
||||
placeholder={i18n.CONNECTOR_SELECTOR_PLACEHOLDER}
|
||||
popoverProps={{ panelMinWidth: 400, anchorPosition: 'downRight' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConnectorSelector.displayName = 'ConnectorSelector';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CONNECTOR_SELECTOR_TITLE = i18n.translate(
|
||||
'securitySolutionPackages.connectors.connectorSelector.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Connector Selector',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NEW_CONNECTOR = i18n.translate(
|
||||
'securitySolutionPackages.connectors.connectorSelector.newConnectorOptions',
|
||||
{
|
||||
defaultMessage: 'Add new Connector...',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_CONNECTOR = i18n.translate(
|
||||
'securitySolutionPackages.connectors.connectorSelector.addConnectorButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECTOR_SELECTOR_PLACEHOLDER = i18n.translate(
|
||||
'securitySolutionPackages.connectors.connectorSelectorInline.connectorPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select a connector',
|
||||
}
|
||||
);
|
19
x-pack/solutions/security/packages/connectors/tsconfig.json
Normal file
19
x-pack/solutions/security/packages/connectors/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
|
@ -19,6 +19,7 @@ const LocalStorageKey = {
|
|||
selectedIntegrationTabId: 'securitySolution.onboarding.selectedIntegrationTabId',
|
||||
selectedCardItemId: 'securitySolution.onboarding.selectedCardItem',
|
||||
integrationSearchTerm: 'securitySolution.onboarding.integrationSearchTerm',
|
||||
assistantConnectorId: 'securitySolution.onboarding.assistantCard.connectorId',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
@ -80,3 +81,9 @@ export const useStoredIntegrationSearchTerm = (spaceId: string) =>
|
|||
`${LocalStorageKey.integrationSearchTerm}.${spaceId}`,
|
||||
null
|
||||
);
|
||||
|
||||
/**
|
||||
* Stores the integration search term per space
|
||||
*/
|
||||
export const useStoredAssistantConnectorId = (spaceId: string) =>
|
||||
useDefinedLocalStorage<string | null>(`${LocalStorageKey.assistantConnectorId}.${spaceId}`, null);
|
||||
|
|
|
@ -8,15 +8,25 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { useAssistantContext, type Conversation } from '@kbn/elastic-assistant';
|
||||
import { useCurrentConversation } from '@kbn/elastic-assistant/impl/assistant/use_current_conversation';
|
||||
import { useDataStreamApis } from '@kbn/elastic-assistant/impl/assistant/use_data_stream_apis';
|
||||
import { getDefaultConnector } from '@kbn/elastic-assistant/impl/assistant/helpers';
|
||||
import { getGenAiConfig } from '@kbn/elastic-assistant/impl/connectorland/helpers';
|
||||
import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation';
|
||||
import { CenteredLoadingSpinner } from '../../../../../common/components/centered_loading_spinner';
|
||||
import { OnboardingCardId } from '../../../../constants';
|
||||
import type { OnboardingCardComponent } from '../../../../types';
|
||||
import * as i18n from './translations';
|
||||
import { useStoredAssistantConnectorId } from '../../../hooks/use_stored_state';
|
||||
import { useOnboardingContext } from '../../../onboarding_context';
|
||||
import { OnboardingCardContentPanel } from '../common/card_content_panel';
|
||||
import { ConnectorCards } from '../common/connectors/connector_cards';
|
||||
import { CardCallOut } from '../common/card_callout';
|
||||
import { CardSubduedText } from '../common/card_subdued_text';
|
||||
import type { AssistantCardMetadata } from './types';
|
||||
import { MissingPrivilegesCallOut } from '../common/connectors/missing_privileges';
|
||||
import type { AIConnector } from '../common/connectors/types';
|
||||
|
||||
export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
|
||||
isCardComplete,
|
||||
|
@ -25,6 +35,9 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
|
|||
checkComplete,
|
||||
isCardAvailable,
|
||||
}) => {
|
||||
const { spaceId } = useOnboardingContext();
|
||||
const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata ?? {};
|
||||
|
||||
const isIntegrationsCardComplete = useMemo(
|
||||
() => isCardComplete(OnboardingCardId.integrations),
|
||||
[isCardComplete]
|
||||
|
@ -39,9 +52,99 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
|
|||
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
|
||||
}, [setExpandedCardId]);
|
||||
|
||||
const connectors = checkCompleteMetadata?.connectors;
|
||||
const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors;
|
||||
const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors;
|
||||
const [selectedConnectorId, setSelectedConnectorId] = useStoredAssistantConnectorId(spaceId);
|
||||
|
||||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const {
|
||||
http,
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
baseConversations,
|
||||
getLastConversationId,
|
||||
} = useAssistantContext();
|
||||
const {
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
isFetchedCurrentUserConversations,
|
||||
isFetchedPrompts,
|
||||
refetchCurrentUserConversations,
|
||||
} = useDataStreamApis({ http, baseConversations, isAssistantEnabled });
|
||||
|
||||
const { currentConversation, handleOnConversationSelected } = useCurrentConversation({
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
defaultConnector,
|
||||
refetchCurrentUserConversations,
|
||||
conversationId: getLastConversationId(),
|
||||
mayUpdateConversations:
|
||||
isFetchedCurrentUserConversations &&
|
||||
isFetchedPrompts &&
|
||||
Object.keys(conversations).length > 0,
|
||||
});
|
||||
|
||||
const onConversationChange = useCallback(
|
||||
(updatedConversation: Conversation) => {
|
||||
handleOnConversationSelected({
|
||||
cId: updatedConversation.id,
|
||||
cTitle: updatedConversation.title,
|
||||
});
|
||||
},
|
||||
[handleOnConversationSelected]
|
||||
);
|
||||
|
||||
const onConnectorSelected = useCallback(
|
||||
async (connector: AIConnector) => {
|
||||
const connectorId = connector.id;
|
||||
|
||||
const config = getGenAiConfig(connector);
|
||||
const apiProvider = config?.apiProvider;
|
||||
const model = config?.defaultModel;
|
||||
|
||||
if (currentConversation != null) {
|
||||
const conversation = await setApiConfig({
|
||||
conversation: currentConversation,
|
||||
apiConfig: {
|
||||
...currentConversation.apiConfig,
|
||||
actionTypeId: connector.actionTypeId,
|
||||
connectorId,
|
||||
// With the inline component, prefer config args to handle 'new connector' case
|
||||
provider: apiProvider,
|
||||
model,
|
||||
},
|
||||
});
|
||||
|
||||
if (conversation && onConversationChange != null) {
|
||||
onConversationChange(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedConnectorId != null) {
|
||||
setSelectedConnectorId(connectorId);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentConversation,
|
||||
selectedConnectorId,
|
||||
setApiConfig,
|
||||
onConversationChange,
|
||||
setSelectedConnectorId,
|
||||
]
|
||||
);
|
||||
|
||||
if (!checkCompleteMetadata) {
|
||||
return (
|
||||
<OnboardingCardContentPanel>
|
||||
<CenteredLoadingSpinner />
|
||||
</OnboardingCardContentPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const onNewConnectorSaved = (connectorId: string) => {
|
||||
checkComplete();
|
||||
setSelectedConnectorId(connectorId);
|
||||
};
|
||||
|
||||
return (
|
||||
<OnboardingCardContentPanel>
|
||||
|
@ -77,7 +180,9 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
|
|||
<ConnectorCards
|
||||
canCreateConnectors={canCreateConnectors}
|
||||
connectors={connectors}
|
||||
onConnectorSaved={checkComplete}
|
||||
onNewConnectorSaved={onNewConnectorSaved}
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
onConnectorSelected={onConnectorSelected}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -18,7 +18,7 @@ export const ASSISTANT_CARD_DESCRIPTION = i18n.translate(
|
|||
'xpack.securitySolution.onboarding.assistantCard.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose and configure any AI provider available to use with Elastic AI Assistant.',
|
||||
'The Elastic AI connector is currently configured, powered by OpenAI gpt 4.0 for optimal performance for migrating SIEM rules. However,any AI service provider can be configured. Read more about AI provider performance and other Elastic powered by AI features.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -6,42 +6,39 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { type AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import {
|
||||
CreateConnectorPopover,
|
||||
type CreateConnectorPopoverProps,
|
||||
} from './create_connector_popover';
|
||||
import { ConnectorSetup } from './connector_setup';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { AIConnector } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { MissingPrivilegesDescription } from './missing_privileges';
|
||||
import { ConnectorSetup } from './connector_setup';
|
||||
import { ConnectorSelectorPanel } from './connector_selector_panel';
|
||||
|
||||
interface ConnectorCardsProps
|
||||
extends CreateConnectorPopoverProps,
|
||||
Omit<ConnectorListProps, 'connectors'> {
|
||||
interface ConnectorCardsProps {
|
||||
onNewConnectorSaved: (connectorId: string) => void;
|
||||
canCreateConnectors?: boolean;
|
||||
connectors?: AIConnector[]; // make connectors optional to handle loading state
|
||||
selectedConnectorId?: string | null;
|
||||
onConnectorSelected: (connector: AIConnector) => void;
|
||||
}
|
||||
|
||||
export const ConnectorCards = React.memo<ConnectorCardsProps>(
|
||||
({
|
||||
connectors,
|
||||
onConnectorSaved,
|
||||
onNewConnectorSaved,
|
||||
canCreateConnectors,
|
||||
selectedConnectorId,
|
||||
setSelectedConnectorId,
|
||||
onConnectorSelected,
|
||||
}) => {
|
||||
const onNewConnectorStoredSave = useCallback(
|
||||
(newConnector: AIConnector) => {
|
||||
onNewConnectorSaved(newConnector.id);
|
||||
// default select the new connector created
|
||||
onConnectorSelected(newConnector);
|
||||
},
|
||||
[onConnectorSelected, onNewConnectorSaved]
|
||||
);
|
||||
|
||||
if (!connectors) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
@ -59,95 +56,26 @@ export const ConnectorCards = React.memo<ConnectorCardsProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
{hasConnectors ? (
|
||||
<>
|
||||
<ConnectorList
|
||||
connectors={connectors}
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
setSelectedConnectorId={setSelectedConnectorId}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<CreateConnectorPopover
|
||||
canCreateConnectors={canCreateConnectors}
|
||||
onConnectorSaved={onConnectorSaved}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ConnectorSetup onConnectorSaved={onConnectorSaved} />
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
height: 160px;
|
||||
`}
|
||||
>
|
||||
{hasConnectors && (
|
||||
<EuiFlexItem>
|
||||
<ConnectorSelectorPanel
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
connectors={connectors}
|
||||
onConnectorSelected={onConnectorSelected}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<ConnectorSetup onConnectorSaved={onNewConnectorStoredSave} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConnectorCards.displayName = 'ConnectorCards';
|
||||
|
||||
interface ConnectorListProps {
|
||||
connectors: AIConnector[];
|
||||
selectedConnectorId?: string | null;
|
||||
setSelectedConnectorId?: (id: string) => void;
|
||||
}
|
||||
|
||||
const ConnectorList = React.memo<ConnectorListProps>(
|
||||
({ connectors, selectedConnectorId, setSelectedConnectorId }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { actionTypeRegistry } = useKibana().services.triggersActionsUi;
|
||||
const onConnectorClick = useCallback(
|
||||
(id: string) => {
|
||||
setSelectedConnectorId?.(id);
|
||||
},
|
||||
[setSelectedConnectorId]
|
||||
);
|
||||
|
||||
const selectedCss = `border: 2px solid ${euiTheme.colors.primary};`;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
wrap
|
||||
gutterSize="s"
|
||||
className={css`
|
||||
padding: ${euiTheme.size.s} 0;
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
>
|
||||
{connectors.map((connector) => (
|
||||
<EuiFlexItem
|
||||
key={connector.id}
|
||||
grow={false}
|
||||
className={css`
|
||||
width: 30%;
|
||||
`}
|
||||
>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
paddingSize="m"
|
||||
onClick={setSelectedConnectorId ? () => onConnectorClick(connector.id) : undefined}
|
||||
css={css`
|
||||
${selectedConnectorId === connector.id ? selectedCss : ''}
|
||||
`}
|
||||
color={selectedConnectorId === connector.id ? 'primary' : 'plain'}
|
||||
>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" wrap>
|
||||
<EuiFlexItem
|
||||
className={css`
|
||||
min-width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiText size="s">{connector.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">
|
||||
{actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConnectorList.displayName = 'ConnectorList';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { ConnectorSelectorWithIcon } from './connector_selector_with_icon';
|
||||
import * as i18n from './translations';
|
||||
import type { AIConnector } from './types';
|
||||
|
||||
interface ConnectorSelectorPanelProps {
|
||||
connectors: AIConnector[];
|
||||
selectedConnectorId?: string | null;
|
||||
onConnectorSelected: (connector: AIConnector) => void;
|
||||
}
|
||||
|
||||
export const ConnectorSelectorPanel = React.memo<ConnectorSelectorPanelProps>(
|
||||
({ connectors, selectedConnectorId, onConnectorSelected }) => {
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false} justifyContent="center">
|
||||
<EuiText>{i18n.SELECTED_PROVIDER}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem justifyContent="center">
|
||||
<ConnectorSelectorWithIcon
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
connectors={connectors}
|
||||
onConnectorSelected={onConnectorSelected}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConnectorSelectorPanel.displayName = 'ConnectorSelectorPanel';
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useMemo, useEffect, useCallback } from 'react';
|
||||
import { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import { ConnectorSelector } from '@kbn/security-solution-connectors';
|
||||
import {
|
||||
getActionTypeTitle,
|
||||
getGenAiConfig,
|
||||
} from '@kbn/elastic-assistant/impl/connectorland/helpers';
|
||||
import { css } from '@emotion/react';
|
||||
import type { AIConnector } from './types';
|
||||
import { useFilteredActionTypes } from './hooks/use_load_action_types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
selectedConnectorId?: string | null;
|
||||
connectors: AIConnector[];
|
||||
onConnectorSelected: (connector: AIConnector) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A compact wrapper of the ConnectorSelector with a Selected Icon
|
||||
*/
|
||||
export const ConnectorSelectorWithIcon = React.memo<Props>(
|
||||
({ isDisabled = false, selectedConnectorId, connectors, onConnectorSelected }) => {
|
||||
const { actionTypeRegistry, assistantAvailability } = useAssistantContext();
|
||||
|
||||
const actionTypes = useFilteredActionTypes();
|
||||
|
||||
const selectedConnector = useMemo(
|
||||
() => connectors.find((connector) => connector.id === selectedConnectorId),
|
||||
[connectors, selectedConnectorId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectors.length === 1) {
|
||||
onConnectorSelected(connectors[0]);
|
||||
}
|
||||
}, [connectors, onConnectorSelected]);
|
||||
|
||||
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
|
||||
|
||||
const connectorOptions = useMemo(
|
||||
() =>
|
||||
(connectors ?? []).map((connector) => {
|
||||
const connectorTypeTitle =
|
||||
getGenAiConfig(connector)?.apiProvider ??
|
||||
getActionTypeTitle(actionTypeRegistry.get(connector.actionTypeId));
|
||||
const connectorDetails = connector.isPreconfigured
|
||||
? i18n.PRECONFIGURED_CONNECTOR
|
||||
: connectorTypeTitle;
|
||||
|
||||
return {
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connectorDetails,
|
||||
};
|
||||
}),
|
||||
[actionTypeRegistry, connectors]
|
||||
);
|
||||
|
||||
const onConnectorSelectionChange = useCallback(
|
||||
(connectorId: string) => {
|
||||
const connector = (connectors ?? []).find((c) => c.id === connectorId);
|
||||
if (connector) {
|
||||
onConnectorSelected(connector);
|
||||
}
|
||||
},
|
||||
[connectors, onConnectorSelected]
|
||||
);
|
||||
|
||||
if (!actionTypes) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
css={css`
|
||||
height: 32px;
|
||||
`}
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
>
|
||||
{selectedConnector && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(selectedConnector.actionTypeId).iconClass}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{selectedConnectorId && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorSelector
|
||||
connectors={connectorOptions}
|
||||
isDisabled={localIsDisabled}
|
||||
selectedId={selectedConnectorId}
|
||||
onChange={onConnectorSelectionChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConnectorSelectorWithIcon.displayName = 'ConnectorSelectorWithIcon';
|
|
@ -6,142 +6,105 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { type ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import {
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiListGroup,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiTextColor,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
ConnectorAddModal,
|
||||
type ActionConnector,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import type { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { AddConnectorModal } from '@kbn/elastic-assistant/impl/connectorland/add_connector_modal';
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import { useFilteredActionTypes } from './hooks/use_load_action_types';
|
||||
|
||||
const usePanelCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
.connectorSelectorPanel {
|
||||
height: 160px;
|
||||
&.euiPanel:hover {
|
||||
background-color: ${euiTheme.colors.backgroundBaseSubdued};
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
interface ConnectorSetupProps {
|
||||
onConnectorSaved?: (savedAction: ActionConnector) => void;
|
||||
onClose?: () => void;
|
||||
compressed?: boolean;
|
||||
}
|
||||
export const ConnectorSetup = React.memo<ConnectorSetupProps>(
|
||||
({ onConnectorSaved, onClose, compressed = false }) => {
|
||||
const panelCss = usePanelCss();
|
||||
const {
|
||||
http,
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
export const ConnectorSetup = React.memo<ConnectorSetupProps>(({ onConnectorSaved, onClose }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
|
||||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
|
||||
const onModalClose = useCallback(() => {
|
||||
setSelectedActionType(null);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
const {
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
} = useKibana().services;
|
||||
|
||||
const actionTypes = useFilteredActionTypes(http, toasts);
|
||||
const onModalClose = useCallback(() => {
|
||||
setSelectedActionType(null);
|
||||
setIsModalVisible(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
if (!actionTypes) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
const actionTypes = useFilteredActionTypes();
|
||||
|
||||
return (
|
||||
<>
|
||||
{compressed ? (
|
||||
<EuiListGroup
|
||||
flush
|
||||
data-test-subj="connectorSetupCompressed"
|
||||
listItems={actionTypes.map((actionType) => ({
|
||||
key: actionType.id,
|
||||
id: actionType.id,
|
||||
label: actionType.name,
|
||||
size: 's',
|
||||
icon: (
|
||||
<EuiIcon
|
||||
size="l"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(actionType.id).iconClass}
|
||||
/>
|
||||
),
|
||||
isDisabled: !actionType.enabled,
|
||||
onClick: () => setSelectedActionType(actionType),
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
{actionTypes.map((actionType: ActionType) => (
|
||||
<EuiFlexItem key={actionType.id}>
|
||||
<EuiLink
|
||||
color="text"
|
||||
onClick={() => setSelectedActionType(actionType)}
|
||||
data-test-subj={`actionType-${actionType.id}`}
|
||||
className={panelCss}
|
||||
>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
paddingSize="m"
|
||||
className="connectorSelectorPanel"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gutterSize="s"
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(actionType.id).iconClass}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTextColor color="default">
|
||||
<EuiText size="s">{actionType.name}</EuiText>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
{selectedActionType && (
|
||||
<ConnectorAddModal
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionType={selectedActionType}
|
||||
onClose={onModalClose}
|
||||
postSaveEventHandler={onConnectorSaved}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (!actionTypes) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
style={{ height: '100%' }}
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
{actionTypes.map((actionType: ActionType) => (
|
||||
<EuiFlexItem grow={false} key={actionType.id}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gutterSize="s"
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(actionType.id).iconClass}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="createConnectorButton"
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
isLoading={false}
|
||||
>
|
||||
{i18n.CREATE_NEW_CONNECTOR_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{isModalVisible && onConnectorSaved && (
|
||||
<AddConnectorModal
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={actionTypes}
|
||||
onClose={onModalClose}
|
||||
onSaveConnector={onConnectorSaved}
|
||||
onSelectActionType={setSelectedActionType}
|
||||
selectedActionType={selectedActionType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
ConnectorSetup.displayName = 'ConnectorSetup';
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { EuiPopover, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { ConnectorSetup } from './connector_setup';
|
||||
import * as i18n from './translations';
|
||||
import { MissingPrivilegesTooltip } from './missing_privileges';
|
||||
|
||||
export interface CreateConnectorPopoverProps {
|
||||
onConnectorSaved: () => void;
|
||||
canCreateConnectors?: boolean;
|
||||
}
|
||||
|
||||
export const CreateConnectorPopover = React.memo<CreateConnectorPopoverProps>(
|
||||
({ onConnectorSaved, canCreateConnectors }) => {
|
||||
const [isOpen, setIsPopoverOpen] = useState(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const onButtonClick = useCallback(
|
||||
() => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen),
|
||||
[]
|
||||
);
|
||||
if (!canCreateConnectors) {
|
||||
return (
|
||||
<MissingPrivilegesTooltip>
|
||||
<EuiLink data-test-subj="createConnectorPopoverButton" onClick={onButtonClick} disabled>
|
||||
{i18n.ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER}
|
||||
</EuiLink>
|
||||
</MissingPrivilegesTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
className={css`
|
||||
width: fit-content;
|
||||
`}
|
||||
button={
|
||||
<EuiText size="s">
|
||||
<EuiLink data-test-subj="createConnectorPopoverButton" onClick={onButtonClick}>
|
||||
{i18n.ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="createConnectorPopover"
|
||||
>
|
||||
<ConnectorSetup onConnectorSaved={onConnectorSaved} onClose={closePopover} compressed />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
CreateConnectorPopover.displayName = 'CreateConnectorPopover';
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import { useLoadActionTypes as loadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import { AIActionTypeIds } from '../constants';
|
||||
|
||||
export const useFilteredActionTypes = (http: HttpSetup, toasts: IToasts) => {
|
||||
export const useFilteredActionTypes = () => {
|
||||
const { http, toasts } = useAssistantContext();
|
||||
const { data } = loadActionTypes({ http, toasts });
|
||||
return useMemo(() => data?.filter(({ id }) => AIActionTypeIds.includes(id)), [data]);
|
||||
};
|
||||
|
|
|
@ -7,10 +7,16 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER = i18n.translate(
|
||||
export const CREATE_NEW_CONNECTOR_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover',
|
||||
{
|
||||
defaultMessage: 'Create new connector',
|
||||
defaultMessage: 'AI service provider',
|
||||
}
|
||||
);
|
||||
export const SELECTED_PROVIDER = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.assistantCard.selectedProvider',
|
||||
{
|
||||
defaultMessage: 'Selected provider',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -21,6 +27,13 @@ export const PRIVILEGES_MISSING_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PRECONFIGURED_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.assistantCard.preconfiguredTitle',
|
||||
{
|
||||
defaultMessage: 'Preconfigured',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIVILEGES_REQUIRED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import type { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
|
||||
export type AIConnector = ActionConnector & {
|
||||
// related to OpenAI connectors, ex: Azure OpenAI, OpenAI
|
||||
apiProvider?: OpenAiProviderType;
|
||||
};
|
|
@ -17,6 +17,7 @@ import { ConnectorCards } from '../../common/connectors/connector_cards';
|
|||
import { CardSubduedText } from '../../common/card_subdued_text';
|
||||
import type { AIConnectorCardMetadata } from './types';
|
||||
import { MissingPrivilegesCallOut } from '../../common/connectors/missing_privileges';
|
||||
import type { AIConnector } from '../../common/connectors/types';
|
||||
|
||||
export const AIConnectorCard: OnboardingCardComponent<AIConnectorCardMetadata> = ({
|
||||
checkCompleteMetadata,
|
||||
|
@ -24,13 +25,13 @@ export const AIConnectorCard: OnboardingCardComponent<AIConnectorCardMetadata> =
|
|||
setComplete,
|
||||
}) => {
|
||||
const { siemMigrations } = useKibana().services;
|
||||
const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage<string | null>(
|
||||
const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage<string>(
|
||||
siemMigrations.rules.connectorIdStorage.key,
|
||||
null
|
||||
''
|
||||
);
|
||||
const setSelectedConnectorId = useCallback(
|
||||
(connectorId: string) => {
|
||||
setStoredConnectorId(connectorId);
|
||||
const setSelectedConnector = useCallback(
|
||||
(connector: AIConnector) => {
|
||||
setStoredConnectorId(connector.id);
|
||||
setComplete(true);
|
||||
},
|
||||
[setComplete, setStoredConnectorId]
|
||||
|
@ -57,9 +58,9 @@ export const AIConnectorCard: OnboardingCardComponent<AIConnectorCardMetadata> =
|
|||
<ConnectorCards
|
||||
canCreateConnectors={canCreateConnectors}
|
||||
connectors={connectors}
|
||||
onConnectorSaved={checkComplete}
|
||||
onNewConnectorSaved={checkComplete}
|
||||
selectedConnectorId={storedConnectorId}
|
||||
setSelectedConnectorId={setSelectedConnectorId}
|
||||
onConnectorSelected={setSelectedConnector}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -230,6 +230,7 @@
|
|||
"@kbn/react-hooks",
|
||||
"@kbn/index-adapter",
|
||||
"@kbn/core-http-server-utils",
|
||||
"@kbn/security-solution-connectors",
|
||||
"@kbn/core-chrome-browser-mocks",
|
||||
"@kbn/ai-assistant-icon",
|
||||
"@kbn/llm-tasks-plugin",
|
||||
|
|
|
@ -7124,6 +7124,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/security-solution-connectors@link:x-pack/solutions/security/packages/connectors":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/security-solution-distribution-bar@link:x-pack/solutions/security/packages/distribution_bar":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue