From 2af57fb24e588dc46d57a5636aef3c25cf0b8aed Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Fri, 1 Sep 2023 10:00:09 -0500 Subject: [PATCH 01/14] [Serverless Search] Getting Started: Separate cloud id panel (#165170) ## Summary Moved the cloud id from the API key panel to its own cloud details panel with the url as well. ### Screenshots ![image](https://github.com/elastic/kibana/assets/1972968/9db88c94-7285-4e7d-8a4b-106a8f1707f1) ![image](https://github.com/elastic/kibana/assets/1972968/20486279-3f49-44d7-bf9d-c63bb39dabe1) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../components/api_key/api_key.scss | 3 + .../components/api_key/api_key.tsx | 193 +++++++----------- .../application/components/overview.test.tsx | 26 ++- .../application/components/overview.tsx | 76 ++++++- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 7 files changed, 167 insertions(+), 143 deletions(-) create mode 100644 x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss diff --git a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss new file mode 100644 index 0000000000000..b74c06cd58030 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.scss @@ -0,0 +1,3 @@ +.apiKeySuccessPanel { + background-color: transparentize($euiColorSuccess, .9); +} diff --git a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx index 07d93fb8bcfe2..07675f41e3f07 100644 --- a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx @@ -14,10 +14,9 @@ import { EuiIcon, EuiPanel, EuiSpacer, - EuiSplitPanel, EuiStep, EuiText, - EuiThemeProvider, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -29,9 +28,10 @@ import { useKibanaServices } from '../../hooks/use_kibana'; import { MANAGEMENT_API_KEYS } from '../../routes'; import { CreateApiKeyFlyout } from './create_api_key_flyout'; import { CreateApiKeyResponse } from './types'; +import './api_key.scss'; export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: string) => void }) => { - const { cloud, http, userProfile } = useKibanaServices(); + const { http, userProfile } = useKibanaServices(); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const { data } = useQuery({ queryKey: ['apiKey'], @@ -53,7 +53,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri /> )} {apiKey ? ( - + ) : ( - - - {i18n.translate('xpack.serverlessSearch.apiKey.stepOneDescription', { - defaultMessage: 'Unique identifier for authentication and authorization. ', + +

+ {i18n.translate('xpack.serverlessSearch.apiKey.panel.title', { + defaultMessage: 'Prepare an API Key', })} - - - - - - - - setIsFlyoutOpen(true)} - > - - {i18n.translate('xpack.serverlessSearch.apiKey.newButtonLabel', { - defaultMessage: 'New', - })} - - - - - - - - {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { - defaultMessage: 'Manage', +

+
+ + {i18n.translate('xpack.serverlessSearch.apiKey.panel.description', { + defaultMessage: + 'An API key is a private, unique identifier for authentication and authorization.', + })} + + + + + + + + setIsFlyoutOpen(true)} + > + + {i18n.translate('xpack.serverlessSearch.apiKey.newButtonLabel', { + defaultMessage: 'New', })} - - +
+ + + + + + + {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { + defaultMessage: 'Manage', + })} + + + + + + + {!!data?.apiKeys && ( + + + + + + + 0 ? 'success' : 'warning'}> + {data.apiKeys.length} + + ), + }} + /> + - - - {!!data?.apiKeys && ( - - - - - - - 0 ? 'success' : 'warning'}> - {data.apiKeys.length} - - ), - }} - /> - - - - )} - - -
+ )} + +
)} - - - - - - {i18n.translate('xpack.serverlessSearch.apiKey.stepTwoDescription', { - defaultMessage: 'Unique identifier for specific project. ', - })} - - - - - - - {cloud.cloudId} - - - - ); }; diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx index 0d1cf8efa67c8..28748db40cedb 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.test.tsx @@ -10,7 +10,25 @@ import { ElasticsearchOverview as Overview } from './overview'; describe('', () => { beforeEach(() => { - core.http.fetch.mockResolvedValueOnce({ apiKeys: [] }); + core.http.fetch.mockImplementation((url) => { + let fetchedUrl: string; + if (typeof url === 'string') { + fetchedUrl = url; + } + + return new Promise((resolve, reject) => { + switch (fetchedUrl) { + case '/internal/serverless_search/api_keys': + resolve({ apiKeys: [] }); + return; + case '/internal/serverless_search/connectors': + resolve({}); + return; + default: + return reject(`unknown path requested ${fetchedUrl}`); + } + }); + }); }); test('renders without throwing an error', () => { @@ -33,7 +51,11 @@ describe('', () => { }); test('api key', () => { const { getByRole } = render(); - expect(getByRole('heading', { name: 'Store your API key and Cloud ID' })).toBeDefined(); + expect(getByRole('heading', { level: 2, name: 'Prepare an API Key' })).toBeDefined(); + }); + test('cloud id', () => { + const { getByRole } = render(); + expect(getByRole('heading', { name: 'Copy your connection details' })).toBeDefined(); }); test('configure client', () => { const { getByRole } = render(); diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.tsx index 0fb599c770044..ddcc4e08cce13 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.tsx @@ -9,11 +9,15 @@ import { EuiAvatar, EuiButtonEmpty, EuiCard, + EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiPageTemplate, EuiPanel, + EuiSpacer, EuiText, + EuiThemeProvider, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -107,19 +111,71 @@ export const ElasticsearchOverview = () => { } - links={[ - { - href: docLinks.securityApis, - label: i18n.translate('xpack.serverlessSearch.configureClient.basicConfigLabel', { - defaultMessage: 'Basic configuration', - }), - }, - ]} + links={[]} title={i18n.translate('xpack.serverlessSearch.apiKey.title', { - defaultMessage: 'Store your API key and Cloud ID', + defaultMessage: 'Prepare an API Key', + })} + /> + + + + + +
+ {i18n.translate('xpack.serverlessSearch.cloudIdDetails.id.title', { + defaultMessage: 'Cloud ID', + })} +
+
+ + + + + {cloud.cloudId} + + + +
+ + +
+ {i18n.translate('xpack.serverlessSearch.cloudIdDetails.url.title', { + defaultMessage: 'Cloud URL', + })} +
+
+ + + + + {cloud.elasticsearchUrl} + + + +
+ + } + links={[]} + title={i18n.translate('xpack.serverlessSearch.cloudIdDetails.title', { + defaultMessage: 'Copy your connection details', })} />
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f28b7badca2b2..a7c61f6934865 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34613,10 +34613,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "Découvrir comment structurer les descripteurs de rôles", "xpack.serverlessSearch.apiKey.setup.description": "Les détails de la configuration de base pour créer votre clé d’API.", "xpack.serverlessSearch.apiKey.setup.title": "Configuration", - "xpack.serverlessSearch.apiKey.stepOneDescription": "Identifiant unique pour l’authentification et l’autorisation. ", - "xpack.serverlessSearch.apiKey.stepOneTitle": "Générer et stocker vos clés d’API", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "Identifiant unique pour un projet spécifique. ", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "Stocker votre identifiant unique du cloud", "xpack.serverlessSearch.apiKey.title": "Stocker votre clé d’API et votre identifiant du cloud", "xpack.serverlessSearch.apiKey.userFieldHelpText": "Identifiant de l’utilisateur créant la clé d’API.", "xpack.serverlessSearch.apiKey.userFieldLabel": "Utilisateur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a811a6af0c24f..4248b9bc241b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34612,10 +34612,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "ロール記述子を構造化する方法をご覧ください", "xpack.serverlessSearch.apiKey.setup.description": "APIキーを作成するための基本構成詳細情報。", "xpack.serverlessSearch.apiKey.setup.title": "セットアップ", - "xpack.serverlessSearch.apiKey.stepOneDescription": "認証と認可のための一意の識別子。", - "xpack.serverlessSearch.apiKey.stepOneTitle": "APIキーを生成して保存", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "特定のプロジェクトの一意の識別子。", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "一意のCloud IDを保存", "xpack.serverlessSearch.apiKey.title": "APIキーとCloud IDを保存", "xpack.serverlessSearch.apiKey.userFieldHelpText": "APIキーを作成するユーザーのID。", "xpack.serverlessSearch.apiKey.userFieldLabel": "ユーザー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a85154c01ca4d..4eb582abc2cd2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34608,10 +34608,6 @@ "xpack.serverlessSearch.apiKey.roleDescriptorsLinkLabel": "了解如何构造角色描述符", "xpack.serverlessSearch.apiKey.setup.description": "用于创建 API 密钥的基本配置详情。", "xpack.serverlessSearch.apiKey.setup.title": "设置", - "xpack.serverlessSearch.apiKey.stepOneDescription": "用于身份验证和授权的唯一标识符。", - "xpack.serverlessSearch.apiKey.stepOneTitle": "生成并存储 API 密钥", - "xpack.serverlessSearch.apiKey.stepTwoDescription": "用于特定项目的唯一标识符。", - "xpack.serverlessSearch.apiKey.stepTwoTitle": "存储您的唯一云 ID", "xpack.serverlessSearch.apiKey.title": "存储您的 API 密钥和云 ID", "xpack.serverlessSearch.apiKey.userFieldHelpText": "创建 API 密钥的用户的 ID。", "xpack.serverlessSearch.apiKey.userFieldLabel": "用户", From 43b3f2e1cfc6883e0c53c7061b04fa36caa113d6 Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Fri, 1 Sep 2023 11:02:29 -0400 Subject: [PATCH 02/14] [Fleet] Classify index not found as file not found too (#165469) --- x-pack/plugins/fleet/server/services/files/client_from_host.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/files/client_from_host.ts b/x-pack/plugins/fleet/server/services/files/client_from_host.ts index b2e607a819875..4329743f92fbf 100644 --- a/x-pack/plugins/fleet/server/services/files/client_from_host.ts +++ b/x-pack/plugins/fleet/server/services/files/client_from_host.ts @@ -81,6 +81,9 @@ export class FleetFromHostFilesClient implements FleetFromHostFileClientInterfac if (error instanceof FleetFilesClientError) { throw error; } + if (error.message.includes('index_not_found')) { + throw new FleetFileNotFound(error.message, error); + } throw new FleetFilesClientError(error.message, error); } From 555f8656944cac53db36cb561b28dd5638fe08eb Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Fri, 1 Sep 2023 18:03:50 +0200 Subject: [PATCH 03/14] test api/status for capacity (#164864) ## Summary Adding API capacity test for `GET /api/status` Stats: ``` "warmupAvgResponseTime": 39, "rpsAtWarmup":10, "warmupDuration":30, "rpsMax":695, "responseTimeMetric":"85%", "threshold1ResponseTime":1000, "rpsAtThreshold1":435, "threshold2ResponseTime":3000, "rpsAtThreshold2":475, "threshold3ResponseTime":5000, "rpsAtThreshold3":495 ``` --- x-pack/test/scalability/apis/api.status.json | 48 +++++++++++++++++++ .../scalability/apis/api.status.no_auth.json | 47 ++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 x-pack/test/scalability/apis/api.status.json create mode 100644 x-pack/test/scalability/apis/api.status.no_auth.json diff --git a/x-pack/test/scalability/apis/api.status.json b/x-pack/test/scalability/apis/api.status.json new file mode 100644 index 0000000000000..396d9d0645fc3 --- /dev/null +++ b/x-pack/test/scalability/apis/api.status.json @@ -0,0 +1,48 @@ +{ + "journeyName": "POST /api/status", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 1000, + "threshold2": 3000, + "threshold3": 5000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 10, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 10, + "maxUsersCount": 700, + "duration": "138s" + } + ], + "maxDuration": "6m" + }, + "testData": { + "esArchives": [], + "kbnArchives": [] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/api/status", + "headers": { + "Cookie": "", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json; charset=utf-8" + }, + "statusCode": 200 + } + } + ] + } + ] +} diff --git a/x-pack/test/scalability/apis/api.status.no_auth.json b/x-pack/test/scalability/apis/api.status.no_auth.json new file mode 100644 index 0000000000000..2fe293f072a25 --- /dev/null +++ b/x-pack/test/scalability/apis/api.status.no_auth.json @@ -0,0 +1,47 @@ +{ + "journeyName": "POST /api/status", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 1000, + "threshold2": 3000, + "threshold3": 5000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 10, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 10, + "maxUsersCount": 1210, + "duration": "80s" + } + ], + "maxDuration": "4m" + }, + "testData": { + "esArchives": [], + "kbnArchives": [] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/api/status", + "headers": { + "Accept-Encoding": "gzip", + "Content-Type": "application/json; charset=utf-8" + }, + "statusCode": 200 + } + } + ] + } + ] +} From 44a3864876f85ad33cfac62b56ab9910f46010c5 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 1 Sep 2023 18:13:00 +0200 Subject: [PATCH 04/14] [Logs] use ISO timestamp for Assistant messages (#165475) --- .../alert_details_app_section/components/log_rate_analysis.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx index 170cd511f463e..8dc2688956a49 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx @@ -230,7 +230,7 @@ export const LogRateAnalysis: FC = ({ r Do not mention indidivual p-values from the analysis results. Do not guess, just say what you are sure of. Do not repeat the given instructions in your output.`; - const now = new Date().toString(); + const now = new Date().toISOString(); return [ { From 9090ceaf491a120d21b65ced5596f9fe07e0e196 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Fri, 1 Sep 2023 18:25:16 +0200 Subject: [PATCH 05/14] [Observability Ai Assistant] Onboarding & Action Menu Improvements (#165459) --- .../buttons/ask_assistant_button.tsx | 6 + .../components/chat/chat_actions_menu.tsx | 238 ++++++++++++++++++ .../public/components/chat/chat_body.tsx | 53 ++-- .../public/components/chat/chat_flyout.tsx | 3 + .../public/components/chat/chat_header.tsx | 85 ++++--- .../components/chat/chat_timeline.stories.tsx | 12 + .../public/components/chat/chat_timeline.tsx | 5 +- .../components/chat/chat_welcome_panel.tsx | 37 ++- .../chat/experimental_feature_banner.tsx | 6 +- .../components/chat/initial_setup_panel.tsx | 219 ++++++++++++++++ .../observability_ai_assistant/public/i18n.ts | 7 + .../conversations/conversation_view.tsx | 3 + .../utils/get_models_management_href.ts | 12 + 13 files changed, 621 insertions(+), 65 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts diff --git a/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx index cbd95b9b80fe5..c8125cd91b5fc 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/buttons/ask_assistant_button.tsx @@ -83,6 +83,12 @@ export function AskAssistantButton({ )} > void; +}) { + const [isOpen, setIsOpen] = useState(false); + + const toggleActionsMenu = () => { + setIsOpen(!isOpen); + }; + + return ( + + } + panelPaddingSize="none" + closePopover={toggleActionsMenu} + > + + {i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + defaultMessage: 'Connector', + })}{' '} + + { + connectors.connectors?.find(({ id }) => id === connectors.selectedConnector) + ?.name + } + + + ), + panel: 1, + }, + { + name: ( + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', + { + defaultMessage: 'Knowledge base', + } + )} + + + {knowledgeBase.status.loading || knowledgeBase.isInstalling ? ( + + ) : knowledgeBase.status.value?.ready ? ( + + ) : ( + + )} + + + ), + panel: 2, + }, + { + name: i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.copyConversation', + { + defaultMessage: 'Copy conversation', + } + ), + disabled: !conversationId, + onClick: () => { + toggleActionsMenu(); + onCopyConversationClick(); + }, + }, + ], + }, + { + id: 1, + width: 256, + title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + defaultMessage: 'Connector', + }), + content: ( + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement.button', + { + defaultMessage: 'Manage connectors', + } + )} + + + ), + }, + { + id: 2, + width: 256, + title: i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.title', + { + defaultMessage: 'Knowledge base', + } + ), + content: ( + + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.description.paragraph', + { + defaultMessage: + 'Using a knowledge base is optional but improves the experience of using the Assistant significantly.', + } + )}{' '} + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.elser.learnMore', + { + defaultMessage: 'Learn more', + } + )} + +

+
+ + + {knowledgeBase.isInstalling || knowledgeBase.status.loading ? ( + + ) : ( + <> + { + if (e.target.checked) { + knowledgeBase.install(); + } + }} + /> + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement', + { + defaultMessage: 'Go to Machine Learning', + } + )} + + + )} +
+ ), + }, + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 14e9df56935dd..6242e5e8472d3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -24,19 +24,14 @@ import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; import { useTimeline } from '../../hooks/use_timeline'; import { useLicense } from '../../hooks/use_license'; import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; -import { MissingCredentialsCallout } from '../missing_credentials_callout'; import { ExperimentalFeatureBanner } from './experimental_feature_banner'; +import { InitialSetupPanel } from './initial_setup_panel'; import { IncorrectLicensePanel } from './incorrect_license_panel'; import { ChatHeader } from './chat_header'; import { ChatPromptEditor } from './chat_prompt_editor'; import { ChatTimeline } from './chat_timeline'; import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; -const containerClassName = css` - max-height: 100%; - max-width: ${1200 - 250}px; // page template max width - conversation list width. -`; - const timelineClassName = css` overflow-y: auto; `; @@ -57,6 +52,7 @@ export function ChatBody({ connectors, knowledgeBase, connectorsManagementHref, + modelsManagementHref, conversationId, currentUser, startedFrom, @@ -70,6 +66,7 @@ export function ChatBody({ connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; connectorsManagementHref: string; + modelsManagementHref: string; conversationId?: string; currentUser?: Pick; startedFrom?: StartedFrom; @@ -83,10 +80,10 @@ export function ChatBody({ const chatService = useObservabilityAIAssistantChatService(); const timeline = useTimeline({ - messages, + chatService, connectors, currentUser, - chatService, + messages, startedFrom, onChatUpdate, onChatComplete, @@ -100,6 +97,13 @@ export function ChatBody({ connectors.loading || knowledgeBase.status.loading || last(timeline.items)?.loading ); + const containerClassName = css` + max-height: 100%; + max-width: ${startedFrom === 'conversationView' + ? 1200 - 250 + 'px' // page template max width - conversation list width. + : '100%'}; + `; + useEffect(() => { const parent = timelineContainerRef.current?.parentElement; if (!parent) { @@ -143,6 +147,12 @@ export function ChatBody({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [timelineContainerRef.current]); + const handleCopyConversation = () => { + const content = JSON.stringify({ title, messages }); + + navigator.clipboard?.writeText(content || ''); + }; + if (!hasCorrectLicense && !conversationId) { footer = ( <> @@ -166,12 +176,14 @@ export function ChatBody({ ); - } else if (connectors.connectors?.length === 0) { + } else if (connectors.connectors?.length === 0 && !conversationId) { footer = ( - <> - - - + ); } else { footer = ( @@ -181,6 +193,7 @@ export function ChatBody({ - - - + {connectors.selectedConnector ? ( + + + + ) : null} + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx index 7f8924995f07b..023370999b470 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx @@ -15,6 +15,7 @@ import { useKibana } from '../../hooks/use_kibana'; import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; +import { getModelsManagementHref } from '../../utils/get_models_management_href'; import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { ChatBody } from './chat_body'; @@ -102,6 +103,8 @@ export function ChatFlyout({ messages={messages} currentUser={currentUser} connectorsManagementHref={getConnectorsManagementHref(http)} + modelsManagementHref={getModelsManagementHref(http)} + conversationId={conversationId} knowledgeBase={knowledgeBase} startedFrom={startedFrom} onChatUpdate={(nextMessages) => { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx index 0314380a87657..5ee7006fec88f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx @@ -16,12 +16,12 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantAvatar } from '../assistant_avatar'; -import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; -import { EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; -import { KnowledgeBaseCallout } from './knowledge_base_callout'; +import { ChatActionsMenu } from './chat_actions_menu'; +import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import { useUnmountAndRemountWhenPropChanges } from '../../hooks/use_unmount_and_remount_when_prop_changes'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; // needed to prevent InlineTextEdit component from expanding container const minWidthClassName = css` @@ -33,19 +33,33 @@ export function ChatHeader({ loading, licenseInvalid, connectors, + connectorsManagementHref, + modelsManagementHref, + conversationId, knowledgeBase, + startedFrom, onSaveTitle, + onCopyConversation, }: { title: string; loading: boolean; licenseInvalid: boolean; connectors: UseGenAIConnectorsResult; + connectorsManagementHref: string; + modelsManagementHref: string; + conversationId?: string; knowledgeBase: UseKnowledgeBaseResult; + startedFrom?: StartedFrom; + onCopyConversation: () => void; onSaveTitle?: (title: string) => void; }) { const hasTitle = !!title; - const displayedTitle = licenseInvalid ? UPGRADE_LICENSE_TITLE : title || EMPTY_CONVERSATION_TITLE; + const displayedTitle = !connectors.selectedConnector + ? ASSISTANT_SETUP_TITLE + : licenseInvalid + ? UPGRADE_LICENSE_TITLE + : title || EMPTY_CONVERSATION_TITLE; const theme = useEuiTheme(); @@ -57,45 +71,40 @@ export function ChatHeader({ return ( - + {loading ? : } - - - {shouldRender ? ( - - ) : null} - - - - - - - - {!licenseInvalid ? : null} - - - - + {shouldRender ? ( + + ) : null} + + + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx index abbbb6806fd38..c276744caef46 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx @@ -48,6 +48,18 @@ const Template: ComponentStory = (props: ChatTimelineProps) => }; const defaultProps: ComponentProps = { + knowledgeBase: { + status: { + loading: false, + value: { + ready: true, + }, + refresh: () => {}, + }, + isInstalling: false, + installError: undefined, + install: async () => {}, + }, items: [ buildChatInitItem(), buildUserChatItem(), diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index f471fa6768c70..8e50f11842e5d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -14,6 +14,7 @@ import { ChatItem } from './chat_item'; import { ChatWelcomePanel } from './chat_welcome_panel'; import type { Feedback } from '../feedback_buttons'; import type { Message } from '../../../common'; +import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; export interface ChatTimelineItem extends Pick { @@ -37,6 +38,7 @@ export interface ChatTimelineItem export interface ChatTimelineProps { items: ChatTimelineItem[]; + knowledgeBase: UseKnowledgeBaseResult; onEdit: (item: ChatTimelineItem, message: Message) => Promise; onFeedback: (item: ChatTimelineItem, feedback: Feedback) => void; onRegenerate: (item: ChatTimelineItem) => void; @@ -45,6 +47,7 @@ export interface ChatTimelineProps { export function ChatTimeline({ items = [], + knowledgeBase, onEdit, onFeedback, onRegenerate, @@ -77,7 +80,7 @@ export function ChatTimeline({ /> )) )} - {filteredItems.length === 1 ? : null} + {filteredItems.length === 1 ? : null} ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx index 32fba0b72a828..3525758da810f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_welcome_panel.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-theme'; import ctaImage from '../../assets/elastic_ai_assistant.png'; +import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; const incorrectLicenseContainer = css` height: 100%; padding: ${euiThemeVars.euiPanelPaddingModifiers.paddingMedium}; `; -export function ChatWelcomePanel() { +export function ChatWelcomePanel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { return ( - +

{i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.title', { @@ -36,12 +37,34 @@ export function ChatWelcomePanel() {

- {i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.body', { - defaultMessage: - 'Elastic AI Assistant is an experimental feature. Make sure to provide feedback when you interact with it.', - })} + {knowledgeBase.status.value?.ready + ? i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.body.kbReady', { + defaultMessage: + 'Keep in mind that Elastic AI Assistant is a technical preview feature. Please provide feedback at any time.', + }) + : i18n.translate('xpack.observabilityAiAssistant.chatWelcomePanel.body.kbNotReady', { + defaultMessage: + 'We recommend you enable the knowledge base for a better experience. It will provide the assistant with the ability to learn from your interaction with it. Keep in mind that Elastic AI Assistant is a technical preview feature. Please provide feedback at any time.', + })}

+ + {!knowledgeBase.status.value?.ready ? ( + + {i18n.translate( + 'xpack.observabilityAiAssistant.chatWelcomePanel.knowledgeBase.buttonLabel.notInstalledYet', + { + defaultMessage: 'Set up knowledge base', + } + )} + + ) : null} ); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx index 48935a49ccf02..c3ddc4f885e32 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/experimental_feature_banner.tsx @@ -20,7 +20,7 @@ import illustration from '../../assets/illustration.svg'; export function ExperimentalFeatureBanner() { return ( - <> +
@@ -33,7 +33,7 @@ export function ExperimentalFeatureBanner() { /> - + - +
); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx new file mode 100644 index 0000000000000..ef492be652b38 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/initial_setup_panel.tsx @@ -0,0 +1,219 @@ +/* + * 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 { + EuiBetaBadge, + EuiButton, + EuiCallOut, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; +import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import { ExperimentalFeatureBanner } from './experimental_feature_banner'; +import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; + +export function InitialSetupPanel({ + connectors, + connectorsManagementHref, + knowledgeBase, + startedFrom, +}: { + connectors: UseGenAIConnectorsResult; + connectorsManagementHref: string; + knowledgeBase: UseKnowledgeBaseResult; + startedFrom?: StartedFrom; +}) { + return ( + <> + + + + + + +

+ {i18n.translate('xpack.observabilityAiAssistant.initialSetupPanel.title', { + defaultMessage: + 'Start your Al experience with Elastic by completing the steps below.', + })} +

+
+ + + + + + } + title={i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.title', + { + defaultMessage: 'Knowledge Base', + } + )} + description={ + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph1', + { + defaultMessage: + 'We recommend you enable the knowledge base for a better experience. It will provide the assistant with the ability to learn from your interaction with it.', + } + )} +

+

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph2', + { + defaultMessage: 'This step is optional, you can always do it later.', + } + )} +

+
+ } + footer={ + knowledgeBase.status.value?.ready ? ( + + ) : ( + + {knowledgeBase.isInstalling || knowledgeBase.status.loading + ? i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.installingKb', + { + defaultMessage: 'Installing knowledge base', + } + ) + : i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.kbNotInstalledYet', + { + defaultMessage: 'Set up knowledge base', + } + )} + + ) + } + /> +
+ + + } + title={i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.title', + { + defaultMessage: 'Connector setup', + } + )} + description={ + !connectors.connectors?.length ? ( + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description1', + { + defaultMessage: 'Set up a Generative AI connector with your AI provider.', + } + )} +

+ +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2', + { + defaultMessage: + 'The Generative AI model needs to support function calls. We strongly recommend using GPT4.', + } + )} + +

+
+ ) : connectors.connectors.length && !connectors.selectedConnector ? ( + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description', + { + defaultMessage: 'Please select a provider.', + } + )} +

+
+ ) : ( + '' + ) + } + footer={ + !connectors.connectors?.length ? ( + + {i18n.translate( + 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel', + { + defaultMessage: 'Set up Generative AI connector', + } + )} + + ) : connectors.connectors.length && !connectors.selectedConnector ? ( + + ) : null + } + /> +
+
+ + + + +

+ {i18n.translate('xpack.observabilityAiAssistant.initialSetupPanel.disclaimer', { + defaultMessage: + 'The AI provider that is configured may collect telemetry when using the Elastic AI Assistant. Contact your AI provider for information on how data is collected.', + })} +

+
+
+ + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/i18n.ts b/x-pack/plugins/observability_ai_assistant/public/i18n.ts index 533e0fff950b9..dcc28d7ff531a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/i18n.ts +++ b/x-pack/plugins/observability_ai_assistant/public/i18n.ts @@ -7,6 +7,13 @@ import { i18n } from '@kbn/i18n'; +export const ASSISTANT_SETUP_TITLE = i18n.translate( + 'xpack.observabilityAiAssistant.assistantSetup.title', + { + defaultMessage: 'Welcome to Elastic AI Assistant', + } +); + export const EMPTY_CONVERSATION_TITLE = i18n.translate( 'xpack.observabilityAiAssistant.emptyConversationTitle', { defaultMessage: 'New conversation' } diff --git a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx index c9331f75244b7..5af2e740deb9b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx @@ -23,6 +23,7 @@ import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_as import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; +import { getModelsManagementHref } from '../../utils/get_models_management_href'; import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; const containerClassName = css` @@ -230,10 +231,12 @@ export function ConversationView() { currentUser={currentUser} connectors={connectors} connectorsManagementHref={getConnectorsManagementHref(http)} + modelsManagementHref={getModelsManagementHref(http)} conversationId={conversationId} knowledgeBase={knowledgeBase} messages={displayedMessages} title={conversation.value.conversation.title} + startedFrom="conversationView" onChatUpdate={(messages) => { setDisplayedMessages(messages); }} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts b/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts new file mode 100644 index 0000000000000..8ff585e005f95 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_models_management_href.ts @@ -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. + */ + +import { HttpStart } from '@kbn/core/public'; + +export function getModelsManagementHref(http: HttpStart) { + return http!.basePath.prepend(`/app/ml/trained_models`); +} From d2381149fdaae6070fa007953f3969924916a6e9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Fri, 1 Sep 2023 18:32:13 +0200 Subject: [PATCH 06/14] [Security Solution] Fix Related integrations Cypress test (#165481) --- .../related_integrations.cy.ts | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts index 2d543bcfd35fc..6d4cf36a40d4a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts @@ -221,14 +221,29 @@ describe('Related integrations', { tags: ['@ess', '@brokenInServerless'] }, () = openTable(); filterBy(RELATED_INTEGRATION_FIELD); - RULE_RELATED_INTEGRATIONS.forEach((integration) => { - cy.contains( - FIELD(RELATED_INTEGRATION_FIELD), - `{"package":"${integration.package}"${ - integration.integration ? `,"integration":"${integration.integration}"` : '' - },"version":"${integration.version}"}` - ); - }); + cy.get(FIELD(RELATED_INTEGRATION_FIELD)) + .invoke('text') + .then((stringValue) => { + // Integrations are displayed in the flyout as a string with a format like so: + // '{"package":"aws","version":"1.17.0","integration":"unknown"}{"package":"mock","version":"1.1.0"}{"package":"system","version":"1.17.0"}' + // We need to parse it to an array of valid objects before we can compare it to the expected value + // Otherwise, the test might fail because of the order of the properties in the objects in the string + const jsonStringArray = stringValue.split('}{'); + + const validJsonStringArray = createValidJsonStringArray(jsonStringArray); + + const parsedIntegrations = validJsonStringArray.map((jsonString) => + JSON.parse(jsonString) + ); + + RULE_RELATED_INTEGRATIONS.forEach((integration) => { + expect(parsedIntegrations).to.deep.include({ + package: integration.package, + version: integration.version, + ...(integration.integration ? { integration: integration.integration } : {}), + }); + }); + }); }); }); }); @@ -407,3 +422,14 @@ const AWS_PACKAGE_POLICY: PackagePolicyWithoutAgentPolicyId = { }, }, }; + +const createValidJsonStringArray = (jsonStringArray: string[]) => + jsonStringArray.map((jsonString, index) => { + if (index === 0) { + return `${jsonString}}`; + } else if (index === jsonStringArray.length - 1) { + return `{${jsonString}`; + } else { + return `{${jsonString}}`; + } + }); From cf464a91b0d743ff577d7a0555e4b2c03dbf7807 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 1 Sep 2023 18:54:53 +0200 Subject: [PATCH 07/14] [Jest tests] Update create serverless root helpers to use `kill` (#165467) ## Summary Follow up on https://github.com/elastic/kibana/pull/165316, address https://github.com/elastic/kibana/pull/165316#discussion_r1312403704 * Use the `kill` option in Jest integration test utilities for serverless roots * Fix a typo in a type import * Add the `waitForReady` flag to optionally wait until the cluster is ready to server requests (default: `false`) ## TODO - [x] Add a test for `waitForReady` --- .../src/create_serverless_root.ts | 20 +--------- .../kbn-es/src/cli_commands/serverless.ts | 4 +- packages/kbn-es/src/cluster.js | 4 +- packages/kbn-es/src/utils/docker.test.ts | 21 ++++++++++ packages/kbn-es/src/utils/docker.ts | 40 ++++++++++++++++++- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts index aee8e77b0397a..3e12b16832714 100644 --- a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts +++ b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_serverless_root.ts @@ -78,9 +78,9 @@ function createServerlessES() { teardown: true, background: true, clean: true, + kill: true, + waitForReady: true, }); - // runServerless doesn't wait until the nodes are up - await waitUntilClusterReady(getServerlessESClient()); return { getClient: getServerlessESClient, stop: async () => { @@ -91,22 +91,6 @@ function createServerlessES() { }; } -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -const waitUntilClusterReady = async (client: Client, timeoutMs = 60 * 1000) => { - const started = Date.now(); - - while (started + timeoutMs > Date.now()) { - try { - await client.info(); - break; - } catch (e) { - await delay(1000); - /* trap to continue */ - } - } -}; - const getServerlessESClient = () => { return new Client({ // node ports not configurable from diff --git a/packages/kbn-es/src/cli_commands/serverless.ts b/packages/kbn-es/src/cli_commands/serverless.ts index 6acb0af250dda..51cc1b619017a 100644 --- a/packages/kbn-es/src/cli_commands/serverless.ts +++ b/packages/kbn-es/src/cli_commands/serverless.ts @@ -11,7 +11,7 @@ import getopts from 'getopts'; import { ToolingLog } from '@kbn/tooling-log'; import { getTimeReporter } from '@kbn/ci-stats-reporter'; -import { Cluster } from '../cluster'; +import { Cluster, type ServerlessOptions } from '../cluster'; import { SERVERLESS_REPO, SERVERLESS_TAG, SERVERLESS_IMG, DEFAULT_PORT } from '../utils'; import { Command } from './types'; @@ -58,7 +58,7 @@ export const serverless: Command = { boolean: ['clean', 'ssl', 'kill', 'background'], default: defaults, - }); + }) as unknown as ServerlessOptions; const cluster = new Cluster(); await cluster.runServerless({ diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index e74f23a0a03ef..084cb9c601578 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -36,7 +36,7 @@ const DEFAULT_READY_TIMEOUT = parseTimeoutToMs('1m'); /** @typedef {import('./cluster_exec_options').EsClusterExecOptions} ExecOptions */ /** @typedef {import('./utils').DockerOptions} DockerOptions */ -/** @typedef {import('./utils').ServerlessOptions}ServerlessrOptions */ +/** @typedef {import('./utils').ServerlessOptions}ServerlessOptions */ // listen to data on stream until map returns anything but undefined const first = (stream, map) => @@ -579,8 +579,6 @@ exports.Cluster = class Cluster { * @param {ServerlessOptions} options */ async runServerless(options = {}) { - // Ensure serverless ES nodes are not running - teardownServerlessClusterSync(this._log, options); if (this._process || this._outcome) { throw new Error('ES has already been started'); } diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index c42ac1af577f0..35696e1f91af8 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -34,6 +34,11 @@ import { ESS_RESOURCES_PATHS } from '../paths'; jest.mock('execa'); const execa = jest.requireMock('execa'); +jest.mock('@elastic/elasticsearch', () => { + return { + Client: jest.fn(), + }; +}); const log = new ToolingLog(); const logWriter = new ToolingLogCollectingWriter(); @@ -465,6 +470,22 @@ describe('runServerlessCluster()', () => { // setupDocker execa calls then run three nodes and attach logger expect(execa.mock.calls).toHaveLength(8); }); + describe('waitForReady', () => { + test('should wait for serverless nodes to be ready to serve requests', async () => { + mockFs({ + [baseEsPath]: {}, + }); + execa.mockImplementation(() => Promise.resolve({ stdout: '' })); + const info = jest.fn(); + jest.requireMock('@elastic/elasticsearch').Client.mockImplementation(() => ({ info })); + + info.mockImplementationOnce(() => Promise.reject()); // first call fails + info.mockImplementationOnce(() => Promise.resolve()); // then succeeds + + await runServerlessCluster(log, { basePath: baseEsPath, waitForReady: true }); + expect(info).toHaveBeenCalledTimes(2); + }); + }); }); describe('stopServerlessCluster()', () => { diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 67acdacc29190..01db89a14c6ab 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -10,6 +10,7 @@ import execa from 'execa'; import fs from 'fs'; import Fsp from 'fs/promises'; import { resolve, basename, join } from 'path'; +import { Client, HttpConnection } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { kibanaPackageJson as pkg, REPO_ROOT } from '@kbn/repo-info'; @@ -35,6 +36,7 @@ interface BaseOptions { image?: string; port?: number; ssl?: boolean; + /** Kill running cluster before starting a new cluster */ kill?: boolean; files?: string | string[]; } @@ -44,10 +46,16 @@ export interface DockerOptions extends EsClusterExecOptions, BaseOptions { } export interface ServerlessOptions extends EsClusterExecOptions, BaseOptions { + /** Clean (or delete) all data created by the ES cluster after it is stopped */ clean?: boolean; + /** Path to the directory where the ES cluster will store data */ basePath: string; + /** If this process exits, teardown the ES cluster as well */ teardown?: boolean; + /** Start the ES cluster in the background instead of remaining attached: useful for running tests */ background?: boolean; + /** Wait for the ES cluster to be ready to serve requests */ + waitForReady?: boolean; } interface ServerlessEsNodeArgs { @@ -539,6 +547,30 @@ export async function runServerlessEsNode( ); } +function getESClient( + { node }: { node: string } = { node: `http://localhost:${DEFAULT_PORT}` } +): Client { + return new Client({ + node, + Connection: HttpConnection, + }); +} + +const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); +async function waitUntilClusterReady(timeoutMs = 60 * 1000): Promise { + const started = Date.now(); + const client = getESClient(); + while (started + timeoutMs > Date.now()) { + try { + await client.info(); + break; + } catch (e) { + await delay(1000); + /* trap to continue */ + } + } +} + /** * Runs an ES Serverless Cluster through Docker */ @@ -583,10 +615,16 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO `); log.warning(`Kibana should be started with the SSL flag so that it can authenticate with ES. - See packages/kbn-es/src/ess_resources/README.md for additional information on authentication. + See packages/kbn-es/src/ess_resources/README.md for additional information on authentication. `); } + if (options.waitForReady) { + log.info('Waiting until ES is ready to serve requests...'); + await waitUntilClusterReady(); + log.success('ES is ready'); + } + if (!options.background) { // The ESS cluster has to be started detached, so we attach a logger afterwards for output await execa('docker', ['logs', '-f', SERVERLESS_NODES[0].name], { From 4ba2955f4f685c321af6e033a871b6a699869271 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 1 Sep 2023 13:15:44 -0400 Subject: [PATCH 08/14] Allow kibana-tech-leads manage Serverless Release Pipeline (#165491) --- catalog-info.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catalog-info.yaml b/catalog-info.yaml index 905eb42178b54..1b13f888cf60d 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -156,5 +156,7 @@ spec: teams: kibana-operations: access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ everyone: access_level: READ_ONLY From 88c04e5c94f81c0c7b7ba4b965725a8878480bd9 Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:45:26 -0500 Subject: [PATCH 09/14] [Security Solution] Expandable flyout - Update index field in analyzer preview (#165398) ## Summary This PR addresses a bug that when a rule is created using data view, analyzer preview is blank. This is due to a check on if index exists before rendering the analyzer preview. This PR updated the index field from `kibana.alert.rule.parameter.index` to 'kibana.alert.rule.indices`, the later is introduced in https://github.com/elastic/kibana/pull/130929 and is available when a rule is created using either index patterns or data view. **How to reproduce the bug** - Refer to bug report https://github.com/elastic/kibana/issues/164829 **How to test** - Create a rule using data view - Generate some alerts - Go to alerts page, expand a row in alerts table - Go to Visualization -> Analyzer preview, the analyzer preview tree should be present ![image](https://github.com/elastic/kibana/assets/18648970/cbe0668e-335d-436a-992c-8970e75a3635) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../flyout/right/components/analyzer_preview.test.tsx | 4 ++-- .../public/flyout/right/components/analyzer_preview.tsx | 4 ++-- .../public/flyout/shared/constants/field_names.ts | 1 + .../public/flyout/shared/mocks/mock_context.ts | 7 +++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx index 8d691ad870892..78b0e3c89c0a0 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { TestProviders } from '../../../common/mock'; import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import { mockContextValue } from '../mocks/mock_right_panel_context'; -import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; +import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_context'; import { RightPanelContext } from '../context'; import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; @@ -62,7 +62,7 @@ describe('', () => { expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({ isActiveTimeline: false, documentId: 'ancestors-id', - indices: ['rule-parameters-index'], + indices: ['rule-indices'], }); expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx index 54fa7d6907bdc..3d872640a5a22 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx @@ -10,7 +10,7 @@ import { EuiTreeView } from '@elastic/eui'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import { getTreeNodes } from '../utils/analyzer_helpers'; import { ANALYZER_PREVIEW_TITLE } from './translations'; -import { ANCESTOR_ID, RULE_PARAMETERS_INDEX } from '../../shared/constants/field_names'; +import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names'; import { useRightPanelContext } from '../context'; import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; @@ -38,7 +38,7 @@ export const AnalyzerPreview: React.FC = () => { const processDocumentId = documentId && Array.isArray(documentId.values) ? documentId.values[0] : ''; - const index = find({ category: 'kibana', field: RULE_PARAMETERS_INDEX }, data); + const index = find({ category: 'kibana', field: RULE_INDICES }, data); const indices = index?.values ?? []; const { statsNodes } = useAlertPrevalenceFromProcessTree({ diff --git a/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts b/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts index e9a896a73dded..b663ea41e2069 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/constants/field_names.ts @@ -7,6 +7,7 @@ export const ANCESTOR_ID = 'kibana.alert.ancestors.id'; export const RULE_PARAMETERS_INDEX = 'kibana.alert.rule.parameters.index'; +export const RULE_INDICES = 'kibana.alert.rule.indices'; export const ORIGINAL_EVENT_ID = 'kibana.alert.original_event.id'; export const ENTRY_LEADER_ENTITY_ID = 'process.entry_leader.entity_id'; export const ENTRY_LEADER_START = 'process.entry_leader.start'; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts index 8280fb64df927..46fa9cff49a31 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/mocks/mock_context.ts @@ -81,6 +81,13 @@ export const mockDataFormattedForFieldBrowser = [ originalValue: ['rule-parameters-index'], isObjectArray: false, }, + { + category: 'kibana', + field: 'kibana.alert.rule.indices', + values: ['rule-indices'], + originalValue: ['rule-indices'], + isObjectArray: false, + }, { category: 'process', field: 'process.entity_id', From 8267c56322ece4e973d2b29ef5455fd1f21ac68e Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 1 Sep 2023 19:46:26 +0200 Subject: [PATCH 10/14] [Security Solution] expandable flyout - add paywall to prevalence details (#165382) --- .../components/prevalence_details.test.tsx | 96 ++++++++++++++++- .../left/components/prevalence_details.tsx | 100 +++++++++++++++--- .../left/components/response_details.tsx | 2 +- .../right/components/highlighted_fields.tsx | 2 + .../shared/hooks/use_fetch_prevalence.ts | 17 ++- 5 files changed, 196 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx index e1512b8b7ada1..5c7a900765fe3 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx @@ -11,11 +11,18 @@ import { LeftPanelContext } from '../context'; import { PrevalenceDetails } from './prevalence_details'; import { PREVALENCE_DETAILS_LOADING_TEST_ID, + PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID, + PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_TEST_ID, + PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, } from './test_ids'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { TestProviders } from '../../../common/mock'; +import { licenseService } from '../../../common/hooks/use_license'; jest.mock('../../shared/hooks/use_prevalence'); @@ -27,6 +34,17 @@ jest.mock('react-redux', () => { useDispatch: () => mockDispatch, }; }); +jest.mock('../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); const panelContextValue = { eventId: 'event id', @@ -36,7 +54,13 @@ const panelContextValue = { } as unknown as LeftPanelContext; describe('PrevalenceDetails', () => { - it('should render the table', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + beforeEach(() => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + }); + + it('should render the table with all columns if license is platinum', () => { const field1 = 'field1'; const field2 = 'field2'; (usePrevalence as jest.Mock).mockReturnValue({ @@ -62,7 +86,7 @@ describe('PrevalenceDetails', () => { ], }); - const { getByTestId } = render( + const { getByTestId, getAllByTestId, queryByTestId } = render( @@ -71,6 +95,74 @@ describe('PrevalenceDetails', () => { ); expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(queryByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).not.toBeInTheDocument(); + }); + + it('should render the table with only basic columns if license is not platinum', () => { + const field1 = 'field1'; + const field2 = 'field2'; + (usePrevalence as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [ + { + field: field1, + value: 'value1', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.05, + userPrevalence: 0.1, + }, + { + field: field2, + value: 'value2', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.5, + userPrevalence: 0.05, + }, + ], + }); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + + const { getByTestId, getAllByTestId } = render( + + + + + + ); + + expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).toBeInTheDocument(); }); it('should render loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index b11622b4a4560..11f370e9572a6 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -5,19 +5,25 @@ * 2.0. */ -import React, { useState } from 'react'; +import dateMath from '@elastic/datemath'; +import React, { useMemo, useState } from 'react'; import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui'; import { + EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, + EuiLink, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiSuperDatePicker, + EuiText, EuiToolTip, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useLicense } from '../../../common/hooks/use_license'; import { InvestigateInTimelineButton } from '../../../common/components/event_details/table/investigate_in_timeline_button'; import type { PrevalenceData } from '../../shared/hooks/use_prevalence'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; @@ -63,16 +69,31 @@ export const PREVALENCE_TAB_ID = 'prevalence-details'; const DEFAULT_FROM = 'now-30d'; const DEFAULT_TO = 'now'; -const columns: Array> = [ +interface PrevalenceDetailsRow extends PrevalenceData { + /** + * From datetime selected in the date picker to pass to timeline + */ + from: string; + /** + * To datetime selected in the date picker to pass to timeline + */ + to: string; +} + +const columns: Array> = [ { field: 'field', name: PREVALENCE_TABLE_FIELD_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + render: (field: string) => {field}, + width: '20%', }, { field: 'value', name: PREVALENCE_TABLE_VALUE_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, + render: (value: string) => {value}, + width: '20%', }, { name: ( @@ -84,7 +105,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ getDataProvider(data.field, `timeline-indicator-${data.field}-${data.value}`, data.value), ]; @@ -93,6 +114,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} > <>{data.alertCount} @@ -112,7 +134,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ { ...getDataProvider( @@ -136,6 +158,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} keepDataView // changing dataview from only detections to include non-alerts docs > <>{data.docCount} @@ -158,10 +181,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, render: (hostPrevalence: number) => ( - <> - {Math.round(hostPrevalence * 100)} - {'%'} - + {`${Math.round(hostPrevalence * 100)}%`} ), width: '10%', }, @@ -177,10 +197,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, render: (userPrevalence: number) => ( - <> - {Math.round(userPrevalence * 100)} - {'%'} - + {`${Math.round(userPrevalence * 100)}%`} ), width: '10%', }, @@ -193,12 +210,38 @@ export const PrevalenceDetails: React.FC = () => { const { browserFields, dataFormattedForFieldBrowser, eventId, investigationFields } = useLeftPanelContext(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + // these two are used by the usePrevalence hook to fetch the data const [start, setStart] = useState(DEFAULT_FROM); const [end, setEnd] = useState(DEFAULT_TO); - const onTimeChange = ({ start: s, end: e }: OnTimeChangeProps) => { + // these two are used to pass to timeline + const [absoluteStart, setAbsoluteStart] = useState( + (dateMath.parse(DEFAULT_FROM) || new Date()).toISOString() + ); + const [absoluteEnd, setAbsoluteEnd] = useState( + (dateMath.parse(DEFAULT_TO) || new Date()).toISOString() + ); + + // TODO update the logic to use a single set of start/end dates + // currently as we're using this InvestigateInTimelineButton component we need to pass the timeRange + // as an AbsoluteTimeRange, which requires from/to values + const onTimeChange = ({ start: s, end: e, isInvalid }: OnTimeChangeProps) => { + if (isInvalid) return; + setStart(s); setEnd(e); + + const from = dateMath.parse(s); + if (from && from.isValid()) { + setAbsoluteStart(from.toISOString()); + } + + const to = dateMath.parse(e); + if (to && to.isValid()) { + setAbsoluteEnd(to.toISOString()); + } }; const { loading, error, data } = usePrevalence({ @@ -210,6 +253,12 @@ export const PrevalenceDetails: React.FC = () => { }, }); + // add timeRange to pass it down to timeline + const items = useMemo( + () => data.map((item) => ({ ...item, from: absoluteStart, to: absoluteEnd })), + [data, absoluteStart, absoluteEnd] + ); + if (loading) { return ( { ); } + const upsell = ( + <> + + + + + ), + }} + /> + + + + ); + return ( <> + {!isPlatinumPlus && upsell} { {data.length > 0 ? ( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx index 07153217262b2..6281a34e0d78f 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx @@ -69,7 +69,7 @@ export const ResponseDetails: React.FC = () => { values={{ editRuleLink: ( > = [ field: 'field', name: HIGHLIGHTED_FIELDS_FIELD_COLUMN, 'data-test-subj': 'fieldCell', + width: '50%', }, { field: 'description', name: HIGHLIGHTED_FIELDS_VALUE_COLUMN, 'data-test-subj': 'valueCell', + width: '50%', render: (description: { field: string; values: string[] | null | undefined; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts index 804784728c3f7..3a0f5f824f4b2 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts @@ -11,6 +11,9 @@ import { useQuery } from '@tanstack/react-query'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../common/lib/kibana'; +import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters'; +import { isActiveTimeline } from '../../../helpers'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const QUERY_KEY = 'useFetchFieldValuePairWithAggregation'; @@ -99,7 +102,10 @@ export const useFetchPrevalence = ({ }, } = useKibana(); - const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to); + // retrieves detections and non-detections indices (for example, the alert security index from the current space and 'logs-*' indices) + const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(SourcererScopeName.default)); + + const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to, selectedPatterns); const { data, isLoading, isError } = useQuery( [QUERY_KEY, highlightedFieldsFilters, from, to], @@ -120,7 +126,8 @@ export const useFetchPrevalence = ({ const buildSearchRequest = ( highlightedFieldsFilters: Record, from: string, - to: string + to: string, + selectedPatterns: string[] ): IEsSearchRequest => { const query = buildEsQuery( undefined, @@ -146,14 +153,16 @@ const buildSearchRequest = ( ] ); - return buildAggregationSearchRequest(query, highlightedFieldsFilters); + return buildAggregationSearchRequest(query, highlightedFieldsFilters, selectedPatterns); }; const buildAggregationSearchRequest = ( query: QueryDslQueryContainer, - highlightedFieldsFilters: Record + highlightedFieldsFilters: Record, + selectedPatterns: string[] ): IEsSearchRequest => ({ params: { + index: selectedPatterns, body: { query, aggs: { From 7a1ec0c5c1c40fde994c3662d18ddd65bc7d1c0a Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 1 Sep 2023 13:58:32 -0400 Subject: [PATCH 11/14] Removing the double quotes from the build.filter (#165500) @brianseeders figured this out. We tried out this change by modifying the pipeline directly, and it triggered the pipeline: https://buildkite.com/elastic/kibana-serverless-release --- catalog-info.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog-info.yaml b/catalog-info.yaml index 1b13f888cf60d..00637fb1a039b 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -151,7 +151,7 @@ spec: build_pull_request_forks: false build_tags: true # https://regex101.com/r/tY52jo/1 - filter_condition: 'build.tag =~ "/^deploy@\d+\$/"' + filter_condition: 'build.tag =~ /^deploy@\d+$/' filter_enabled: true teams: kibana-operations: From 39ef9bc81c1f568351d1c744919cf3598b34c54a Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 1 Sep 2023 14:15:49 -0400 Subject: [PATCH 12/14] Skipping pre-command when running the Upload pipeline (#165505) Context: https://elastic.slack.com/archives/C5UDAFZQU/p1693587045671319 --- .buildkite/scripts/lifecycle/pre_command.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.buildkite/scripts/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index b945f08d1dfd9..4ebb348621f5d 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -2,6 +2,11 @@ set -euo pipefail +if [[ "$BUILDKITE_COMMAND" =~ .*"upload".* ]]; then + echo "Skipped pre-command when running the Upload pipeline" + exit 0 +fi + source .buildkite/scripts/common/util.sh echo '--- Setup environment vars' From 9a549a2fffc3b7e7fa1ec73a5702cee6974f92b3 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 1 Sep 2023 20:18:06 +0200 Subject: [PATCH 13/14] GitHub create-deploy-tag workflow: Show commit SHA (#165460) --- .github/workflows/create-deploy-tag.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-deploy-tag.yml b/.github/workflows/create-deploy-tag.yml index a2c0bd0539984..ec06f8c11b49d 100644 --- a/.github/workflows/create-deploy-tag.yml +++ b/.github/workflows/create-deploy-tag.yml @@ -77,6 +77,10 @@ jobs: "type": "mrkdwn", "text": "*Workflow run:*\n" }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" + }, { "type": "mrkdwn", "text": "*Git tag:*\n" @@ -103,7 +107,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*Useful links:*\n\n • \n • " + "text": "*Useful links:*\n\n • \n • " } }, { @@ -162,6 +166,10 @@ jobs: { "type": "mrkdwn", "text": "*Workflow run:*\n" + }, + { + "type": "mrkdwn", + "text": "*Commit:*\n" } ] }, From 75a23c86c53de5ca4b8b0af4356143e65904f857 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 1 Sep 2023 14:26:24 -0400 Subject: [PATCH 14/14] Echoing out BUILDKITE_COMMAND (#165506) --- .buildkite/scripts/lifecycle/pre_command.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.buildkite/scripts/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index 4ebb348621f5d..1552bf51bf3fa 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -2,6 +2,7 @@ set -euo pipefail +echo "BUILDKITE_COMMAND: $BUILDKITE_COMMAND" if [[ "$BUILDKITE_COMMAND" =~ .*"upload".* ]]; then echo "Skipped pre-command when running the Upload pipeline" exit 0