diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 326b5f2d403bd..7df88be18e334 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -10,8 +10,6 @@ xpack.observability.enabled: false xpack.securitySolution.enabled: false xpack.serverless.observability.enabled: false enterpriseSearch.enabled: false -xpack.fleet.enabled: false -xpack.observabilityAIAssistant.enabled: false xpack.osquery.enabled: false ## Fine-tune the search solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides. @@ -88,4 +86,15 @@ xpack.searchInferenceEndpoints.ui.enabled: false xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json # Semantic text UI -xpack.index_management.dev.enableSemanticText: false +xpack.index_management.dev.enableSemanticText: true + +# AI Assistant config +xpack.observabilityAIAssistant.enabled: true +xpack.searchAssistant.enabled: true +xpack.searchAssistant.ui.enabled: true +xpack.observabilityAIAssistant.scope: "search" +xpack.observabilityAIAssistant.enableKnowledgeBase: false +aiAssistantManagementSelection.preferredAIAssistantType: "observability" +xpack.observabilityAiAssistantManagement.logSourcesEnabled: false +xpack.observabilityAiAssistantManagement.spacesEnabled: false +xpack.observabilityAiAssistantManagement.visibilityEnabled: false diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index 1146a9280ac4e..91f1227ce0d9f 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -183,6 +183,7 @@ xpack.apm.featureFlags.storageExplorerAvailable: false ## Set the AI Assistant type aiAssistantManagementSelection.preferredAIAssistantType: "observability" +xpack.observabilityAIAssistant.scope: "observability" # Specify in telemetry the project type telemetry.labels.serverless: observability diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 361892e430afd..1357af980d278 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -28,9 +28,9 @@ a| <> | Send a request to {gemini}. -a| <> +a| <> -| Send a request to {inference}. +| Send a request to {infer}. a| <> diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index cda03f91dfc17..d6ae2aecaf276 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -330,9 +330,6 @@ the minimum and maximum values of a numeric field or a map of a geo field. [[discover:showMultiFields]]`discover:showMultiFields`:: Controls the display of multi-fields in the expanded document view. -[[discover:showLegacyFieldTopValues]]`discover:showLegacyFieldTopValues`:: -To calculate the top values for a field in the sidebar using 500 instead of 5,000 records per shard, turn on this option. - [[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: The default sort direction for time-based data views. diff --git a/docs/management/connectors/action-types/inference.asciidoc b/docs/management/connectors/action-types/inference.asciidoc index 8c7f2840f9c5c..ea8a0be675e18 100644 --- a/docs/management/connectors/action-types/inference.asciidoc +++ b/docs/management/connectors/action-types/inference.asciidoc @@ -1,7 +1,7 @@ [[inference-action-type]] == {infer-cap} connector and action ++++ -{inference} +{infer-cap} ++++ :frontmatter-description: Add a connector that can send requests to {inference}. :frontmatter-tags-products: [kibana] @@ -9,7 +9,8 @@ :frontmatter-tags-user-goals: [configure] -The {infer} connector uses the {es} client to send requests to an {infer} service. The connector uses the <> to send the request. +The {infer} connector uses the {es} client to send requests to an {infer} service. +The connector uses the <> to send the request. [float] [[define-inference-ui]] @@ -19,7 +20,7 @@ You can create connectors in *{stack-manage-app} > {connectors-ui}*. For example [role="screenshot"] image::management/connectors/images/inference-connector.png[{inference} connector] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. + [float] [[inference-connector-configuration]] @@ -44,7 +45,8 @@ while creating or editing the connector in {kib}. For example: [role="screenshot"] image::management/connectors/images/inference-completion-params.png[{infer} params test] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. + + [float] [[inference-connector-actions]] === {infer-cap} connector actions @@ -56,14 +58,17 @@ The {infer} actions have the following configuration properties. Properties depe ==== Completion The following example performs a completion task on the example question. + Input:: The text on which you want to perform the {infer} task. For example: + -[source,text] -- +[source,text] +------------------------------------------------------------ { input: 'What is Elastic?' } +------------------------------------------------------------ -- [float] @@ -71,18 +76,22 @@ The text on which you want to perform the {infer} task. For example: ==== Text embedding The following example performs a text embedding task. + Input:: The text on which you want to perform the {infer} task. For example: + -[source,text] -- +[source,text] +------------------------------------------------------------ { input: 'The sky above the port was the color of television tuned to a dead channel.', task_settings: { input_type: 'ingest' } } +------------------------------------------------------------ -- + Input type:: An optional string that overwrites the connector's default model. @@ -91,16 +100,20 @@ An optional string that overwrites the connector's default model. ==== Reranking The following example performs a reranking task on the example input. + Input:: The text on which you want to perform the {infer} task. Should be a string array. For example: + -[source,text] -- +[source,text] +------------------------------------------------------------ { input: ['luke', 'like', 'leia', 'chewy', 'r2d2', 'star', 'wars'], query: 'star wars main character' } +------------------------------------------------------------ -- + Query:: The search query text. @@ -109,14 +122,17 @@ The search query text. ==== Sparse embedding The following example performs a sparse embedding task on the example sentence. + Input:: The text on which you want to perform the {infer} task. For example: + -[source,text] -- +[source,text] +------------------------------------------------------------ { input: 'The sky above the port was the color of television tuned to a dead channel.' } +------------------------------------------------------------ -- [float] diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index e926007f77f25..9246133f199ef 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -83,7 +83,6 @@ export const DISCOVER_SAMPLE_SIZE_ID = 'discover:sampleSize'; export const DISCOVER_SEARCH_FIELDS_FROM_SOURCE_ID = 'discover:searchFieldsFromSource'; export const DISCOVER_SEARCH_ON_PAGE_LOAD_ID = 'discover:searchOnPageLoad'; export const DISCOVER_SHOW_FIELD_STATISTICS_ID = 'discover:showFieldStatistics'; -export const DISCOVER_SHOW_LEGACY_FIELD_TOP_VALUES_ID = 'discover:showLegacyFieldTopValues'; export const DISCOVER_SHOW_MULTI_FIELDS_ID = 'discover:showMultiFields'; export const DISCOVER_SORT_DEFAULT_ORDER_ID = 'discover:sort:defaultOrder'; export const DOC_TABLE_HIDE_TIME_COLUMNS_ID = 'doc_table:hideTimeColumn'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index a3925b3a04f24..c810a74091458 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -51,7 +51,7 @@ export async function runDockerGenerator( */ if (flags.baseImage === 'wolfi') baseImageName = - 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:18153942f0d6e97bc6131cd557c7ed3be6e892846a5df0760896eb8d15b1b236'; + 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:8cff240b81057968575dd28dab0c3609657cb7e0e60ff017261e5b721fad9e1b'; let imageFlavor = ''; if (flags.baseImage === 'ubi') imageFlavor += `-ubi`; diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index e3a1282ccc1cf..b2decd2293108 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -360,6 +360,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability_onboarding.ui.enabled (boolean?)', 'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)', 'xpack.observabilityAIAssistant.scope (observability?|search?)', + 'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)', + 'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)', + 'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)', 'share.new_version.enabled (boolean?)', 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', /** diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx index ac25fe6c3703a..4a19272e8938b 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx @@ -18,6 +18,7 @@ import { import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { useKibana } from '../hooks/use_kibana'; +import { useKnowledgeBase } from '../hooks'; export function ChatActionsMenu({ connectors, @@ -31,6 +32,7 @@ export function ChatActionsMenu({ onCopyConversationClick: () => void; }) { const { application, http } = useKibana().services; + const knowledgeBase = useKnowledgeBase(); const [isOpen, setIsOpen] = useState(false); const handleNavigateToConnectors = () => { @@ -91,15 +93,19 @@ export function ChatActionsMenu({ defaultMessage: 'Actions', }), items: [ - { - name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { - defaultMessage: 'Manage knowledge base', - }), - onClick: () => { - toggleActionsMenu(); - handleNavigateToSettingsKnowledgeBase(); - }, - }, + ...(knowledgeBase?.status.value?.enabled + ? [ + { + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { + defaultMessage: 'Manage knowledge base', + }), + onClick: () => { + toggleActionsMenu(); + handleNavigateToSettingsKnowledgeBase(); + }, + }, + ] + : []), { name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', { defaultMessage: 'AI Assistant Settings', diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx index 182cb046cba70..3809e97f059b6 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx @@ -37,6 +37,7 @@ const defaultProps: ComponentStoryObj = { loading: false, value: { ready: true, + enabled: true, }, refresh: () => {}, }, diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx index 5b80a34e0bf7b..12cb747d148c4 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx @@ -123,7 +123,7 @@ export function ChatBody({ showLinkToConversationsApp: boolean; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void; - navigateToConversation: (conversationId?: string) => void; + navigateToConversation?: (conversationId?: string) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx index 8d636374ac768..1343f5ed9a4bb 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx @@ -53,7 +53,7 @@ export function ChatFlyout({ initialFlyoutPositionMode?: FlyoutPositionMode; isOpen: boolean; onClose: () => void; - navigateToConversation(conversationId?: string): void; + navigateToConversation?: (conversationId?: string) => void; }) { const { euiTheme } = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -272,10 +272,14 @@ export function ChatFlyout({ conversationList.conversations.refresh(); }} onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode} - navigateToConversation={(newConversationId?: string) => { - if (onClose) onClose(); - navigateToConversation(newConversationId); - }} + navigateToConversation={ + navigateToConversation + ? (newConversationId?: string) => { + if (onClose) onClose(); + navigateToConversation(newConversationId); + } + : undefined + } /> diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx index c9f0588a1c90f..5110eec04c6e6 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx @@ -60,7 +60,7 @@ export function ChatHeader({ onCopyConversation: () => void; onSaveTitle: (title: string) => void; onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void; - navigateToConversation: (nextConversationId?: string) => void; + navigateToConversation?: (nextConversationId?: string) => void; }) { const theme = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -164,31 +164,32 @@ export function ChatHeader({ } /> - - - - + navigateToConversation(conversationId)} - /> - - } - /> - + display="block" + > + navigateToConversation(conversationId)} + /> + + } + /> + + ) : null} ) : null} diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx index 0afb0c7e79fc0..7c04c3ad0bae7 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx @@ -58,6 +58,7 @@ const defaultProps: ComponentProps = { loading: false, value: { ready: true, + enabled: true, }, refresh: () => {}, }, diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx index e87aa161d80c3..84c730129348e 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx @@ -24,6 +24,7 @@ const defaultProps: ComponentStoryObj = { loading: false, value: { ready: false, + enabled: true, }, refresh: () => {}, }, @@ -43,12 +44,15 @@ export const Loading: ComponentStoryObj = merge({}, defaultPro }); export const NotInstalled: ComponentStoryObj = merge({}, defaultProps, { - args: { knowledgeBase: { status: { loading: false, value: { ready: false } } } }, + args: { knowledgeBase: { status: { loading: false, value: { ready: false, enabled: true } } } }, }); export const Installing: ComponentStoryObj = merge({}, defaultProps, { args: { - knowledgeBase: { status: { loading: false, value: { ready: false } }, isInstalling: true }, + knowledgeBase: { + status: { loading: false, value: { ready: false, enabled: true } }, + isInstalling: true, + }, }, }); @@ -57,7 +61,7 @@ export const InstallError: ComponentStoryObj = merge({}, defau knowledgeBase: { status: { loading: false, - value: { ready: false }, + value: { ready: false, enabled: true }, }, isInstalling: false, installError: new Error(), @@ -66,5 +70,5 @@ export const InstallError: ComponentStoryObj = merge({}, defau }); export const Installed: ComponentStoryObj = merge({}, defaultProps, { - args: { knowledgeBase: { status: { loading: false, value: { ready: true } } } }, + args: { knowledgeBase: { status: { loading: false, value: { ready: true, enabled: true } } } }, }); diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx index a449235ba44e6..2ce11d16905af 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -85,8 +85,9 @@ export function WelcomeMessage({ connectors={connectors} onSetupConnectorClick={handleConnectorClick} /> - - + {knowledgeBase.status.value?.enabled ? ( + + ) : null} diff --git a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx index fe71a9585dd1e..fb74ff7647a21 100644 --- a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -25,7 +25,7 @@ const SECOND_SLOT_CONTAINER_WIDTH = 400; interface ConversationViewProps { conversationId?: string; - navigateToConversation: (nextConversationId?: string) => void; + navigateToConversation?: (nextConversationId?: string) => void; getConversationHref?: (conversationId: string) => string; newConversationHref?: string; scopes?: AssistantScope[]; @@ -81,7 +81,9 @@ export const ConversationView: React.FC = ({ const handleConversationUpdate = (conversation: { conversation: { id: string } }) => { if (!conversationId) { updateConversationIdInPlace(conversation.conversation.id); - navigateToConversation(conversation.conversation.id); + if (navigateToConversation) { + navigateToConversation(conversation.conversation.id); + } } handleRefreshConversations(); }; @@ -143,7 +145,7 @@ export const ConversationView: React.FC = ({ isLoading={conversationList.isLoading} onConversationDeleteClick={(deletedConversationId) => { conversationList.deleteConversation(deletedConversationId).then(() => { - if (deletedConversationId === conversationId) { + if (deletedConversationId === conversationId && navigateToConversation) { navigateToConversation(undefined); } }); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts index bcb1725d35109..8859cc716cc52 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts @@ -17,6 +17,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { error: undefined, value: { ready: true, + enabled: true, }, }, }; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx index 0b949fcdbff0e..72d4fa0acf737 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx @@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service'; export interface UseKnowledgeBaseResult { status: AbortableAsyncState<{ ready: boolean; + enabled: boolean; error?: any; deployment_state?: MlDeploymentState; allocation_state?: MlDeploymentAllocationState; diff --git a/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx index cc2fe761d6176..c848935c17a3a 100644 --- a/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx @@ -18,6 +18,7 @@ import { useLastUsedPrompts } from '../hooks/use_last_used_prompts'; import { FunctionListPopover } from '../chat/function_list_popover'; import { PromptEditorFunction } from './prompt_editor_function'; import { PromptEditorNaturalLanguage } from './prompt_editor_natural_language'; +import { useScopes } from '../hooks/use_scopes'; export interface PromptEditorProps { disabled: boolean; @@ -42,6 +43,7 @@ export function PromptEditor({ onSendTelemetry, onSubmit, }: PromptEditorProps) { + const scopes = useScopes(); const containerRef = useRef(null); const [mode, setMode] = useState<'prompt' | 'function'>( @@ -121,16 +123,15 @@ export function PromptEditor({ setInnerMessage(undefined); setMode('prompt'); - onSendTelemetry({ type: ObservabilityAIAssistantTelemetryEventType.UserSentPromptInChat, - payload: message, + payload: { ...message, scopes }, }); } catch (_) { setInnerMessage(oldMessage); setMode(oldMessage.function_call?.name ? 'function' : 'prompt'); } - }, [addLastUsedPrompt, innerMessage, loading, onSendTelemetry, onSubmit]); + }, [addLastUsedPrompt, innerMessage, loading, onSendTelemetry, onSubmit, scopes]); // Submit on Enter useEffect(() => { diff --git a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts index 5028b53b90ec9..00610d6b64b4e 100644 --- a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts @@ -39,3 +39,17 @@ export const useNavigateFindings = () => { export const useNavigateVulnerabilities = () => useNavigate(findingsNavigation.vulnerabilities.path); + +export const useNavigateNativeVulnerabilities = () => { + const navToVulnerabilities = useNavigateVulnerabilities(); + + return useCallback( + (filterParams: NavFilter = {}, groupBy?: string[]) => { + navToVulnerabilities( + { ...filterParams, 'data_stream.dataset': 'cloud_security_posture.vulnerabilities' }, + groupBy + ); + }, + [navToVulnerabilities] + ); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx b/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx index 7746c5f0a4f1b..84f2c9f3726d6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx @@ -14,7 +14,8 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { ColumnsPopover } from './columns_popover'; -describe('ColumnsPopover', () => { +// FLAKY: https://github.com/elastic/kibana/issues/174682 +describe.skip('ColumnsPopover', () => { let appMockRenderer: AppMockRenderer; beforeEach(() => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_statistics.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_statistics.tsx index 114f28ccfc271..de1f7ec3ba37a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_statistics.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_statistics.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { useNavigateNativeVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; import { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common'; import { getSeverityStatusColor } from '@kbn/cloud-security-posture'; import { VulnCounterCard, type VulnCounterCardProps } from '../../components/vuln_counter_card'; @@ -15,7 +15,7 @@ import { useVulnerabilityDashboardApi } from '../../common/api/use_vulnerability import { CompactFormattedNumber } from '../../components/compact_formatted_number'; export const VulnerabilityStatistics = () => { - const navToVulnerabilities = useNavigateVulnerabilities(); + const navToVulnerabilities = useNavigateNativeVulnerabilities(); const getVulnerabilityDashboard = useVulnerabilityDashboardApi(); const stats: VulnCounterCardProps[] = useMemo( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx index a4e3dd38b28a1..c3a5f21867723 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { NavFilter } from '@kbn/cloud-security-posture/src/utils/query_utils'; -import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { useNavigateNativeVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; import { CVSScoreBadge, SeverityStatusBadge } from '@kbn/cloud-security-posture'; import { @@ -33,7 +33,7 @@ import { VULNERABILITY_GROUPING_OPTIONS, VULNERABILITY_FIELDS } from '../../comm export const VulnerabilityTablePanelSection = () => { const getVulnerabilityDashboard = useVulnerabilityDashboardApi(); const { euiTheme } = useEuiTheme(); - const navToVulnerabilities = useNavigateVulnerabilities(); + const navToVulnerabilities = useNavigateNativeVulnerabilities(); const onCellClick = useCallback( (filters: NavFilter) => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_trend_graph.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_trend_graph.tsx index ff610b640cd3f..599928eea88b8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_trend_graph.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_trend_graph.tsx @@ -19,7 +19,7 @@ import { EuiButton, EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { useNavigateNativeVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; import { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common'; import { getSeverityStatusColor } from '@kbn/cloud-security-posture'; @@ -50,7 +50,7 @@ const theme: PartialTheme = { }; const ViewAllButton = () => { - const navToVulnerabilities = useNavigateVulnerabilities(); + const navToVulnerabilities = useNavigateNativeVulnerabilities(); return ( navToVulnerabilities()} size="s"> diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts index 1bb1322be356f..3b4413cd0497a 100644 --- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts @@ -106,9 +106,7 @@ export class EntityClient { }); if (!definition) { - const message = `Unable to find entity definition with [${id}]`; - this.options.logger.error(message); - throw new EntityDefinitionNotFound(message); + throw new EntityDefinitionNotFound(`Unable to find entity definition with [${id}]`); } await uninstallEntityDefinition({ diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index f218030c65ad3..9dbdf6069aff6 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -316,6 +316,16 @@ export const ModelsList: FC = ({ }; }); }); + + setItemIdToExpandedRowMap((prev) => { + // Refresh expanded rows + return Object.fromEntries( + Object.keys(prev).map((modelId) => { + const item = resultItems.find((i) => i.model_id === modelId); + return item ? [modelId, ] : []; + }) + ); + }); } catch (error) { displayErrorToast( error, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 868ca0d5baa0f..bc188737972f3 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -11,7 +11,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; -import { isNumber } from 'lodash'; +import { chunk, isNumber } from 'lodash'; import { ML_INTERNAL_BASE_PATH } from '../../../../common/constants/app'; import type { MlServerDefaults, @@ -388,13 +388,13 @@ export function mlApiProvider(httpService: HttpService) { end, overallScore, }: { - jobId: string; + jobId: string[]; topN: string; bucketSpan: string; start: number; end: number; overallScore?: number; - }) { + }): Promise { const body = JSON.stringify({ topN, bucketSpan, @@ -402,11 +402,31 @@ export function mlApiProvider(httpService: HttpService) { end, ...(overallScore ? { overall_score: overallScore } : {}), }); - return httpService.http({ - path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/${jobId}/results/overall_buckets`, - method: 'POST', - body, - version: '1', + + // Max permitted job_id is 64 characters, so we can fit around 30 jobs per request + const maxJobsPerRequest = 30; + + return Promise.all( + chunk(jobId, maxJobsPerRequest).map((jobIdsChunk) => { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/${jobIdsChunk.join( + ',' + )}/results/overall_buckets`, + method: 'POST', + body, + version: '1', + }); + }) + ).then((responses) => { + // Merge responses + return responses.reduce( + (acc, response) => { + acc.count += response.count; + acc.overall_buckets.push(...response.overall_buckets); + return acc; + }, + { count: 0, overall_buckets: [] } + ); }); }, diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts index 404bc5d2492ee..38ced9a6587ee 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts @@ -34,7 +34,8 @@ describe('Transaction details', () => { cy.loginAsViewerUser(); }); - it('shows transaction name and transaction charts', () => { + // skipping this as it´s been failing a lot lately, more information here https://github.com/elastic/kibana/issues/197386 + it.skip('shows transaction name and transaction charts', () => { cy.intercept('GET', '/internal/apm/services/opbeans-java/transactions/charts/latency?*').as( 'transactionLatencyRequest' ); @@ -106,8 +107,8 @@ describe('Transaction details', () => { ); cy.contains('Create SLO'); }); - - it('shows top errors table', () => { + // skipping this as it´s been failing a lot lately, more information here https://github.com/elastic/kibana/issues/197386 + it.skip('shows top errors table', () => { cy.visitKibana( `/app/apm/services/opbeans-java/transactions/view?${new URLSearchParams({ ...timeRange, diff --git a/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/context.tsx index e661ef2450310..7ec17b3a6cf3b 100644 --- a/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/context.tsx @@ -76,7 +76,7 @@ export function BreadcrumbsContextProvider({ children }: { children: React.React }; }); - useBreadcrumbs(formattedBreadcrumbs, { serverless }); + useBreadcrumbs(formattedBreadcrumbs, { serverless, absoluteProjectStyleBreadcrumbs: false }); return {children}; } diff --git a/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/use_breadcrumb.ts b/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/use_breadcrumb.ts index a6b80fd088bff..846aa1ec70877 100644 --- a/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/use_breadcrumb.ts +++ b/x-pack/plugins/observability_solution/apm/public/context/breadcrumbs/use_breadcrumb.ts @@ -8,8 +8,10 @@ import { useCurrentRoute } from '@kbn/typed-react-router-config'; import { useContext, useEffect, useRef } from 'react'; import { castArray } from 'lodash'; +import useObservable from 'react-use/lib/useObservable'; import { Breadcrumb, BreadcrumbsContext } from './context'; import { useKibanaEnvironmentContext } from '../kibana_environment_context/use_kibana_environment_context'; +import { useKibana } from '../kibana_context/use_kibana'; export function useBreadcrumb( callback: () => Breadcrumb | Breadcrumb[], @@ -17,6 +19,9 @@ export function useBreadcrumb( options?: { omitRootOnServerless?: boolean; omitOnServerless?: boolean } ) { const { isServerlessEnv } = useKibanaEnvironmentContext(); + const { + services: { chrome }, + } = useKibana(); const { omitRootOnServerless = false, omitOnServerless = false } = options || {}; const api = useContext(BreadcrumbsContext); @@ -29,8 +34,11 @@ export function useBreadcrumb( const matchedRoute = useRef(match?.route); + const chromeStyle = useObservable(chrome.getChromeStyle$()); + useEffect(() => { - if (isServerlessEnv && omitOnServerless) { + const isProjectStyle = isServerlessEnv || chromeStyle === 'project'; + if (isProjectStyle && omitOnServerless) { return; } @@ -42,10 +50,9 @@ export function useBreadcrumb( if (matchedRoute.current) { const breadcrumbs = castArray(callback()); - api.set( matchedRoute.current, - isServerlessEnv && omitRootOnServerless && breadcrumbs.length >= 1 + isProjectStyle && omitRootOnServerless && breadcrumbs.length >= 1 ? breadcrumbs.slice(1) : breadcrumbs ); @@ -57,5 +64,5 @@ export function useBreadcrumb( } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [match, ...fnDeps]); + }, [match, chromeStyle, ...fnDeps]); } diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx index a0079568803b6..a7760014dec8c 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -288,5 +288,8 @@ const Wrapper = styled.div<{ right: 50%; transform: translate(50%, -50%); } + .embPanel { + outline: none; + } } `; diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/index.tsx index c4061f05ce91b..750429602aecf 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/index.tsx @@ -60,7 +60,7 @@ export function ExploratoryViewPage({ }), }, ], - { app } + { app, classicOnly: true } ); const kbnUrlStateStorage = useSessionStorage diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx index c6e8790eeff6a..5c187bb6186d6 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx @@ -50,18 +50,15 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { const parentBreadcrumbResolver = useParentBreadcrumbResolver(); const breadcrumbOptions = parentBreadcrumbResolver.getBreadcrumbOptions(asset.type); - useMetricsBreadcrumbs( - [ - { - ...breadcrumbOptions.link, - text: breadcrumbOptions.text, - }, - { - text: asset.name, - }, - ], - { deeperContextServerless: true } - ); + useMetricsBreadcrumbs([ + { + ...breadcrumbOptions.link, + text: breadcrumbOptions.text, + }, + { + text: asset.name, + }, + ]); useEffect(() => { if (trackOnlyOnce.current) { diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_breadcrumbs.ts deleted file mode 100644 index 0cf9efef908bb..0000000000000 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_breadcrumbs.ts +++ /dev/null @@ -1,42 +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 { ChromeBreadcrumb } from '@kbn/core/public'; -import { useEffect } from 'react'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { observabilityTitle } from '../translations'; -import { useKibanaContextForPlugin } from './use_kibana'; - -type AppId = 'logs' | 'metrics'; - -export const useBreadcrumbs = (app: AppId, appTitle: string, extraCrumbs: ChromeBreadcrumb[]) => { - const { - services: { chrome }, - } = useKibanaContextForPlugin(); - - const observabilityLinkProps = useLinkProps({ app: 'observability-overview' }); - const appLinkProps = useLinkProps({ app }); - - useEffect(() => { - const breadcrumbs = [ - { - ...observabilityLinkProps, - text: observabilityTitle, - }, - { - ...appLinkProps, - text: appTitle, - }, - ...extraCrumbs, - ]; - - const docTitle = [...breadcrumbs].reverse().map((breadcrumb) => breadcrumb.text as string); - - chrome.docTitle.change(docTitle); - chrome.setBreadcrumbs(breadcrumbs); - }, [appLinkProps, appTitle, chrome, extraCrumbs, observabilityLinkProps]); -}; diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_logs_breadcrumbs.tsx b/x-pack/plugins/observability_solution/infra/public/hooks/use_logs_breadcrumbs.tsx index d2fc556caa57b..2eba7845b8d24 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_logs_breadcrumbs.tsx +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_logs_breadcrumbs.tsx @@ -6,10 +6,18 @@ */ import { ChromeBreadcrumb } from '@kbn/core/public'; -import { useBreadcrumbs } from './use_breadcrumbs'; +import { useBreadcrumbs, useLinkProps } from '@kbn/observability-shared-plugin/public'; import { LOGS_APP } from '../../common/constants'; import { logsTitle } from '../translations'; export const useLogsBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { - useBreadcrumbs(LOGS_APP, logsTitle, extraCrumbs); + const appLinkProps = useLinkProps({ app: LOGS_APP }); + const breadcrumbs = [ + { + ...appLinkProps, + text: logsTitle, + }, + ...extraCrumbs, + ]; + useBreadcrumbs(breadcrumbs, { classicOnly: true }); }; diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_metrics_breadcrumbs.tsx b/x-pack/plugins/observability_solution/infra/public/hooks/use_metrics_breadcrumbs.tsx index defc8b3210f48..d5a6011a68e8e 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_metrics_breadcrumbs.tsx +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_metrics_breadcrumbs.tsx @@ -5,42 +5,25 @@ * 2.0. */ -import { useEffect, useMemo } from 'react'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { useBreadcrumbs, useLinkProps } from '@kbn/observability-shared-plugin/public'; import { METRICS_APP } from '../../common/constants'; import { metricsTitle } from '../translations'; import { useKibanaContextForPlugin } from './use_kibana'; -export const useMetricsBreadcrumbs = ( - extraCrumbs: ChromeBreadcrumb[], - options?: { deeperContextServerless: boolean } -) => { +export const useMetricsBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { const { services: { serverless }, } = useKibanaContextForPlugin(); const appLinkProps = useLinkProps({ app: METRICS_APP }); - const breadcrumbs = useMemo( - () => [ - { - ...appLinkProps, - text: metricsTitle, - }, - ...extraCrumbs, - ], - [appLinkProps, extraCrumbs] - ); + const breadcrumbs = [ + { + ...appLinkProps, + text: metricsTitle, + }, + ...extraCrumbs, + ]; - useBreadcrumbs(breadcrumbs); - - useEffect(() => { - // For deeper context breadcrumbs in serveless, the `serverless` plugin provides its own breadcrumb service. - // https://docs.elastic.dev/kibana-dev-docs/serverless-project-navigation#breadcrumbs - if (serverless && options?.deeperContextServerless) { - // The initial path is already set in the breadcrumbs - const [, ...serverlessBreadcrumbs] = breadcrumbs; - serverless.setBreadcrumbs(serverlessBreadcrumbs); - } - }, [breadcrumbs, options?.deeperContextServerless, serverless]); + useBreadcrumbs(breadcrumbs, { serverless }); }; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx index 65ab15a5713be..0e6183268ddda 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx @@ -54,18 +54,15 @@ export const MetricDetailPage = () => { }); const breadcrumbOptions = parentBreadcrumbResolver.getBreadcrumbOptions(nodeType); - useMetricsBreadcrumbs( - [ - { - ...breadcrumbOptions.link, - text: breadcrumbOptions.text, - }, - { - text: name, - }, - ], - { deeperContextServerless: true } - ); + useMetricsBreadcrumbs([ + { + ...breadcrumbOptions.link, + text: breadcrumbOptions.text, + }, + { + text: name, + }, + ]); const [sideNav, setSideNav] = useState([]); diff --git a/x-pack/plugins/observability_solution/infra/public/translations.ts b/x-pack/plugins/observability_solution/infra/public/translations.ts index ecb72b3df4b01..3377ae1dd1fa1 100644 --- a/x-pack/plugins/observability_solution/infra/public/translations.ts +++ b/x-pack/plugins/observability_solution/infra/public/translations.ts @@ -39,7 +39,7 @@ export const metricsTitle = i18n.translate('xpack.infra.header.infrastructureTit }); export const inventoryTitle = i18n.translate('xpack.infra.metrics.infrastructureInventoryTitle', { - defaultMessage: 'Infrastructure Inventory', + defaultMessage: 'Infrastructure inventory', }); export const metricsExplorerTitle = i18n.translate('xpack.infra.metrics.metricsExplorerTitle', { diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx index 8f5acee54f57e..9403090b1e213 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx @@ -93,6 +93,7 @@ export function AlertDetails() { triggersActionsUi: { ruleTypeRegistry }, observabilityAIAssistant, uiSettings, + serverless, } = useKibana().services; const { search } = useLocation(); @@ -158,20 +159,23 @@ export function AlertDetails() { } }, [alertDetail, ruleTypeRegistry]); - useBreadcrumbs([ - { - href: http.basePath.prepend(paths.observability.alerts), - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - deepLinkId: 'observability-overview:alerts', - }, - { - text: alertDetail - ? getPageTitle(alertDetail.formatted.fields[ALERT_RULE_CATEGORY]) - : defaultBreadcrumb, - }, - ]); + useBreadcrumbs( + [ + { + href: http.basePath.prepend(paths.observability.alerts), + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + deepLinkId: 'observability-overview:alerts', + }, + { + text: alertDetail + ? getPageTitle(alertDetail.formatted.fields[ALERT_RULE_CATEGORY]) + : defaultBreadcrumb, + }, + ], + { serverless } + ); const onUntrackAlert = () => { setAlertStatus(ALERT_STATUS_UNTRACKED); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx index ef883f40f4902..6607052225555 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx @@ -159,13 +159,18 @@ function InternalAlertsPage() { [alertSearchBarStateProps.rangeFrom, alertSearchBarStateProps.rangeTo, bucketSize, esQuery] ); - useBreadcrumbs([ + useBreadcrumbs( + [ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + }, + ], { - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - }, - ]); + classicOnly: true, + } + ); async function loadRuleStats() { setRuleStatsLoading(true); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx index 37942cdbca7d6..c7b71431050cf 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx @@ -56,13 +56,18 @@ export function OverviewPage() { const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); - useBreadcrumbs([ + useBreadcrumbs( + [ + { + text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', { + defaultMessage: 'Overview', + }), + }, + ], { - text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', { - defaultMessage: 'Overview', - }), - }, - ]); + classicOnly: true, + } + ); const { data: newsFeed } = useFetcher( () => getNewsFeed({ http, kibanaVersion }), diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx index e8270434c12b2..3b2e5d3118c4a 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx @@ -61,6 +61,7 @@ export function RuleDetailsPage() { getRuleDefinition: RuleDefinition, getRuleStatusPanel: RuleStatusPanel, }, + serverless, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -72,24 +73,27 @@ export function RuleDetailsPage() { filterByRuleTypeIds: filteredRuleTypes, }); - useBreadcrumbs([ - { - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - href: basePath.prepend(paths.observability.alerts), - deepLinkId: 'observability-overview:alerts', - }, - { - href: basePath.prepend(paths.observability.rules), - text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { - defaultMessage: 'Rules', - }), - }, - { - text: rule && rule.name, - }, - ]); + useBreadcrumbs( + [ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + href: basePath.prepend(paths.observability.alerts), + deepLinkId: 'observability-overview:alerts', + }, + { + href: basePath.prepend(paths.observability.rules), + text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { + defaultMessage: 'Rules', + }), + }, + { + text: rule && rule.name, + }, + ], + { serverless } + ); const [activeTabId, setActiveTabId] = useState(() => { const searchParams = new URLSearchParams(search); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx index fb257f9f95cde..b94b9a7e4218f 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx @@ -42,6 +42,7 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { getAddRuleFlyout: AddRuleFlyout, getRulesSettingsLink: RulesSettingsLink, }, + serverless, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); const history = useHistory(); @@ -50,20 +51,23 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { const [addRuleFlyoutVisibility, setAddRuleFlyoutVisibility] = useState(false); const [stateRefresh, setRefresh] = useState(new Date()); - useBreadcrumbs([ - { - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - href: http.basePath.prepend('/app/observability/alerts'), - deepLinkId: 'observability-overview:alerts', - }, - { - text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { - defaultMessage: 'Rules', - }), - }, - ]); + useBreadcrumbs( + [ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + href: http.basePath.prepend('/app/observability/alerts'), + deepLinkId: 'observability-overview:alerts', + }, + { + text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { + defaultMessage: 'Rules', + }), + }, + ], + { serverless } + ); const filteredRuleTypes = useGetFilteredRuleTypes(); const { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/index.ts index 86b8f14bde9e4..049c5a1e65fb0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/index.ts @@ -6,6 +6,7 @@ */ import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +import { AssistantScope } from '@kbn/ai-assistant-common'; import type { Message } from '../../common'; import { chatFeedbackEventSchema, ChatFeedback } from './schemas/chat_feedback'; import { insightFeedbackEventSchema, InsightFeedback } from './schemas/insight_feedback'; @@ -17,7 +18,10 @@ const schemas = [chatFeedbackEventSchema, insightFeedbackEventSchema, userSentPr export type TelemetryEventTypeWithPayload = | { type: ObservabilityAIAssistantTelemetryEventType.ChatFeedback; payload: ChatFeedback } | { type: ObservabilityAIAssistantTelemetryEventType.InsightFeedback; payload: InsightFeedback } - | { type: ObservabilityAIAssistantTelemetryEventType.UserSentPromptInChat; payload: Message }; + | { + type: ObservabilityAIAssistantTelemetryEventType.UserSentPromptInChat; + payload: Message & { scopes: AssistantScope[] }; + }; export const registerTelemetryEventTypes = (analytics: AnalyticsServiceSetup) => { schemas.forEach((schema) => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/schemas/common.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/schemas/common.ts index b01a8e05a4ea5..4a2739ef82c35 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/schemas/common.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/analytics/schemas/common.ts @@ -6,9 +6,10 @@ */ import type { RootSchema } from '@kbn/core/public'; +import { AssistantScope } from '@kbn/ai-assistant-common'; import type { Message } from '../../../common'; -export const messageSchema: RootSchema = { +export const messageSchema: RootSchema = { '@timestamp': { type: 'text', _meta: { @@ -74,4 +75,13 @@ export const messageSchema: RootSchema = { }, }, }, + scopes: { + type: 'array', + items: { + type: 'text', + _meta: { + description: 'The scopes that were used when generating the message.', + }, + }, + }, }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts index dc9a780c82a1f..4d0b9fef3f2f4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts @@ -11,6 +11,7 @@ export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), modelId: schema.maybe(schema.string()), scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])), + enableKnowledgeBase: schema.boolean({ defaultValue: true }), }); export type ObservabilityAIAssistantConfig = TypeOf; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 50687920478af..81f4e24d4d21f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -159,11 +159,14 @@ export class ObservabilityAIAssistantPlugin core, taskManager: plugins.taskManager, getModelId, + enableKnowledgeBase: this.config.enableKnowledgeBase, })); service.register(registerFunctions); - addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') }); + if (this.config.enableKnowledgeBase) { + addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') }); + } registerServerRoutes({ core, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts index 6bb024b913cde..1eb1650545781 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -28,6 +28,7 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ handler: async ( resources ): Promise<{ + enabled: boolean; ready: boolean; error?: any; deployment_state?: MlDeploymentState; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index 19a3dd827107b..a050edc8008fb 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -707,14 +707,16 @@ export class ObservabilityAIAssistantClient { queries: Array<{ text: string; boost?: number }>; categories?: string[]; }): Promise<{ entries: RecalledEntry[] }> => { - return this.dependencies.knowledgeBaseService.recall({ - namespace: this.dependencies.namespace, - user: this.dependencies.user, - queries, - categories, - esClient: this.dependencies.esClient, - uiSettingsClient: this.dependencies.uiSettingsClient, - }); + return ( + this.dependencies.knowledgeBaseService?.recall({ + namespace: this.dependencies.namespace, + user: this.dependencies.user, + queries, + categories, + esClient: this.dependencies.esClient, + uiSettingsClient: this.dependencies.uiSettingsClient, + }) || { entries: [] } + ); }; getKnowledgeBaseStatus = () => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index 63e2ee240927c..d1aba4f232b0d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -70,6 +70,7 @@ export class ObservabilityAIAssistantService { private readonly logger: Logger; private readonly getModelId: () => Promise; private kbService?: KnowledgeBaseService; + private enableKnowledgeBase: boolean; private readonly registrations: RegistrationCallback[] = []; @@ -78,36 +79,40 @@ export class ObservabilityAIAssistantService { core, taskManager, getModelId, + enableKnowledgeBase, }: { logger: Logger; core: CoreSetup; taskManager: TaskManagerSetupContract; getModelId: () => Promise; + enableKnowledgeBase: boolean; }) { this.core = core; this.logger = logger; this.getModelId = getModelId; + this.enableKnowledgeBase = enableKnowledgeBase; this.allowInit(); - - taskManager.registerTaskDefinitions({ - [INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: { - title: 'Index queued KB articles', - description: - 'Indexes previously registered entries into the knowledge base when it is ready', - timeout: '30m', - maxAttempts: 2, - createTaskRunner: (context) => { - return { - run: async () => { - if (this.kbService) { - await this.kbService.processQueue(); - } - }, - }; + if (enableKnowledgeBase) { + taskManager.registerTaskDefinitions({ + [INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: { + title: 'Index queued KB articles', + description: + 'Indexes previously registered entries into the knowledge base when it is ready', + timeout: '30m', + maxAttempts: 2, + createTaskRunner: (context) => { + return { + run: async () => { + if (this.kbService) { + await this.kbService.processQueue(); + } + }, + }; + }, }, - }, - }); + }); + } } getKnowledgeBaseStatus() { @@ -237,6 +242,7 @@ export class ObservabilityAIAssistantService { esClient, taskManagerStart: pluginsStart.taskManager, getModelId: this.getModelId, + enabled: this.enableKnowledgeBase, }); this.logger.info('Successfully set up index assets'); @@ -331,58 +337,62 @@ export class ObservabilityAIAssistantService { } addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void { - this.init() - .then(() => { - this.kbService!.queue( - entries.flatMap((entry) => { - const entryWithSystemProperties = { - ...entry, - '@timestamp': new Date().toISOString(), - doc_id: entry.id, - public: true, - confidence: 'high' as const, - type: 'contextual' as const, - is_correction: false, - labels: { - ...entry.labels, - }, - role: KnowledgeBaseEntryRole.Elastic, - }; - - const operations = - 'texts' in entryWithSystemProperties - ? splitKbText(entryWithSystemProperties) - : [ - { - type: KnowledgeBaseEntryOperationType.Index, - document: entryWithSystemProperties, - }, - ]; - - return operations; - }) - ); - }) - .catch((error) => { - this.logger.error( - `Could not index ${entries.length} entries because of an initialisation error` - ); - this.logger.error(error); - }); + if (this.enableKnowledgeBase) { + this.init() + .then(() => { + this.kbService!.queue( + entries.flatMap((entry) => { + const entryWithSystemProperties = { + ...entry, + '@timestamp': new Date().toISOString(), + doc_id: entry.id, + public: true, + confidence: 'high' as const, + type: 'contextual' as const, + is_correction: false, + labels: { + ...entry.labels, + }, + role: KnowledgeBaseEntryRole.Elastic, + }; + + const operations = + 'texts' in entryWithSystemProperties + ? splitKbText(entryWithSystemProperties) + : [ + { + type: KnowledgeBaseEntryOperationType.Index, + document: entryWithSystemProperties, + }, + ]; + + return operations; + }) + ); + }) + .catch((error) => { + this.logger.error( + `Could not index ${entries.length} entries because of an initialisation error` + ); + this.logger.error(error); + }); + } } addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) { - this.addToKnowledgeBaseQueue( - entries.map((entry) => { - return { - ...entry, - labels: { - ...entry.labels, - category: categoryId, - }, - }; - }) - ); + if (this.enableKnowledgeBase) { + this.addToKnowledgeBaseQueue( + entries.map((entry) => { + return { + ...entry, + labels: { + ...entry.labels, + category: categoryId, + }, + }; + }) + ); + } } register(cb: RegistrationCallback) { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index ee977b30f5cc7..7306a0df7c572 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -34,6 +34,7 @@ interface Dependencies { logger: Logger; taskManagerStart: TaskManagerStartContract; getModelId: () => Promise; + enabled: boolean; } export interface RecalledEntry { @@ -92,6 +93,9 @@ export class KnowledgeBaseService { } setup = async () => { + if (!this.dependencies.enabled) { + return; + } const elserModelId = await this.dependencies.getModelId(); const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 }; @@ -113,9 +117,9 @@ export class KnowledgeBaseService { } catch (error) { if (isModelMissingOrUnavailableError(error)) { return false; - } else { - throw error; } + + throw error; } }; @@ -202,6 +206,9 @@ export class KnowledgeBaseService { }; private ensureTaskScheduled() { + if (!this.dependencies.enabled) { + return; + } this.dependencies.taskManagerStart .ensureScheduled({ taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE, @@ -251,7 +258,7 @@ export class KnowledgeBaseService { } async processQueue() { - if (!this._queue.length) { + if (!this._queue.length || !this.dependencies.enabled) { return; } @@ -305,6 +312,9 @@ export class KnowledgeBaseService { } status = async () => { + if (!this.dependencies.enabled) { + return { ready: false, enabled: false }; + } const elserModelId = await this.dependencies.getModelId(); try { @@ -320,11 +330,13 @@ export class KnowledgeBaseService { deployment_state: deploymentState, allocation_state: allocationState, model_name: elserModelId, + enabled: true, }; } catch (error) { return { error: error instanceof errors.ResponseError ? error.body.error : String(error), ready: false, + enabled: true, model_name: elserModelId, }; } @@ -402,6 +414,9 @@ export class KnowledgeBaseService { }): Promise<{ entries: RecalledEntry[]; }> => { + if (!this.dependencies.enabled) { + return { entries: [] }; + } this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); @@ -474,6 +489,9 @@ export class KnowledgeBaseService { namespace: string, user?: { name: string } ): Promise> => { + if (!this.dependencies.enabled) { + return []; + } try { const response = await this.dependencies.esClient.asInternalUser.search({ index: resourceNames.aliases.kb, @@ -514,6 +532,9 @@ export class KnowledgeBaseService { sortBy?: string; sortDirection?: 'asc' | 'desc'; }): Promise<{ entries: KnowledgeBaseEntry[] }> => { + if (!this.dependencies.enabled) { + return { entries: [] }; + } try { const response = await this.dependencies.esClient.asInternalUser.search({ index: resourceNames.aliases.kb, @@ -578,6 +599,9 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }) => { + if (!this.dependencies.enabled) { + return null; + } const res = await this.dependencies.esClient.asInternalUser.search< Pick >({ @@ -607,6 +631,9 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }): Promise => { + if (!this.dependencies.enabled) { + return; + } // for now we want to limit the number of user instructions to 1 per user if (document.type === KnowledgeBaseType.UserInstruction) { const existingId = await this.getExistingUserInstructionId({ @@ -647,6 +674,9 @@ export class KnowledgeBaseService { }: { operations: KnowledgeBaseEntryOperation[]; }): Promise => { + if (!this.dependencies.enabled) { + return; + } this.dependencies.logger.info(`Starting import of ${operations.length} entries`); const limiter = pLimit(5); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 01202b385917a..883317c02274f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -164,7 +164,7 @@ export function NavControl() { onClose={() => { setIsOpen(false); }} - navigateToConversation={(conversationId: string) => { + navigateToConversation={(conversationId?: string) => { application.navigateToUrl( http.basePath.prepend( `/app/observabilityAIAssistant/conversations/${conversationId || ''}` diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc index ddf00c84c0ac3..f42dc2d2074d8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc @@ -6,9 +6,24 @@ "id": "observabilityAiAssistantManagement", "server": true, "browser": true, - "configPath": ["xpack", "observabilityAiAssistantManagement"], - "requiredPlugins": ["management", "observabilityAIAssistant", "observabilityShared"], - "optionalPlugins": ["actions", "home", "serverless", "enterpriseSearch"], - "requiredBundles": ["kibanaReact", "logsDataAccess"] + "configPath": [ + "xpack", + "observabilityAiAssistantManagement" + ], + "requiredPlugins": [ + "actions", + "management", + "observabilityAIAssistant", + "observabilityShared" + ], + "optionalPlugins": [ + "home", + "serverless", + "enterpriseSearch" + ], + "requiredBundles": [ + "kibanaReact", + "logsDataAccess", + ] } } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx index 4522e00fb37d2..9ab40cc467853 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx @@ -15,7 +15,11 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/public'; import { wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; -import { StartDependencies, AiAssistantManagementObservabilityPluginStart } from './plugin'; +import { + StartDependencies, + AiAssistantManagementObservabilityPluginStart, + ConfigSchema, +} from './plugin'; import { aIAssistantManagementObservabilityRouter } from './routes/config'; import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized'; import { AppContextProvider } from './context/app_context'; @@ -23,9 +27,10 @@ import { AppContextProvider } from './context/app_context'; interface MountParams { core: CoreSetup; mountParams: ManagementAppMountParams; + config: ConfigSchema; } -export const mountManagementSection = async ({ core, mountParams }: MountParams) => { +export const mountManagementSection = async ({ core, mountParams, config }: MountParams) => { const [coreStart, startDeps] = await core.getStartServices(); if (!startDeps.observabilityAIAssistant) return () => {}; @@ -46,7 +51,7 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams) - + void; + config: ConfigSchema; } export const AppContext = createContext(null as any); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx index d3941b3cd50d8..a12fb8e12d90f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx @@ -70,6 +70,11 @@ export const render = ( const appContextValue = mocks?.appContextValue ?? { setBreadcrumbs: () => {}, + config: { + logSourcesEnabled: true, + spacesEnabled: true, + visibilityEnabled: true, + }, }; return testLibRender( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts index 56d5051243f69..f61a188f37c62 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts @@ -5,13 +5,26 @@ * 2.0. */ -import { AiAssistantManagementObservabilityPlugin as AiAssistantManagementObservabilityPlugin } from './plugin'; +import { PluginInitializer, PluginInitializerContext } from '@kbn/core-plugins-browser'; +import { + AiAssistantManagementObservabilityPlugin, + AiAssistantManagementObservabilityPluginSetup, + AiAssistantManagementObservabilityPluginStart, + ConfigSchema, + SetupDependencies, + StartDependencies, +} from './plugin'; export type { AiAssistantManagementObservabilityPluginSetup, AiAssistantManagementObservabilityPluginStart, } from './plugin'; -export function plugin() { - return new AiAssistantManagementObservabilityPlugin(); -} +export const plugin: PluginInitializer< + AiAssistantManagementObservabilityPluginSetup, + AiAssistantManagementObservabilityPluginStart, + SetupDependencies, + StartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => { + return new AiAssistantManagementObservabilityPlugin(pluginInitializerContext); +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts index e2e69ef5600cf..88d007045052e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; @@ -35,6 +35,12 @@ export interface StartDependencies { enterpriseSearch?: EnterpriseSearchPublicStart; } +export interface ConfigSchema { + logSourcesEnabled: boolean; + spacesEnabled: boolean; + visibilityEnabled: boolean; +} + export class AiAssistantManagementObservabilityPlugin implements Plugin< @@ -44,6 +50,12 @@ export class AiAssistantManagementObservabilityPlugin StartDependencies > { + private readonly config: ConfigSchema; + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + } + public setup( core: CoreSetup, { home, management, observabilityAIAssistant }: SetupDependencies @@ -78,6 +90,7 @@ export class AiAssistantManagementObservabilityPlugin return mountManagementSection({ core, mountParams, + config: this.config, }); }, }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx index 9a543be1938ea..c4051b9665b57 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx @@ -8,8 +8,24 @@ import React from 'react'; import { coreStartMock, render } from '../../helpers/test_helper'; import { SettingsPage } from './settings_page'; +import { useKnowledgeBase } from '@kbn/ai-assistant'; + +jest.mock('@kbn/ai-assistant'); + +const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock; describe('Settings Page', () => { + const appContextValue = { + config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true }, + setBreadcrumbs: () => {}, + }; + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + enabled: true, + }, + }, + }); it('should navigate to home when not authorized', () => { render(, { coreStart: { @@ -21,13 +37,16 @@ describe('Settings Page', () => { }, }, }, + appContextValue, }); expect(coreStartMock.application.navigateToApp).toBeCalledWith('home'); }); it('should render settings and knowledge base tabs', () => { - const { getByTestId } = render(); + const { getByTestId } = render(, { + appContextValue, + }); expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument(); expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument(); @@ -36,7 +55,7 @@ describe('Settings Page', () => { it('should set breadcrumbs', () => { const setBreadcrumbs = jest.fn(); render(, { - appContextValue: { setBreadcrumbs }, + appContextValue: { ...appContextValue, setBreadcrumbs }, }); expect(setBreadcrumbs).toHaveBeenCalledWith([ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx index 075aaeb0aeb75..57a167b1080fa 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx @@ -8,6 +8,7 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; +import { useKnowledgeBase } from '@kbn/ai-assistant'; import { useAppContext } from '../../hooks/use_app_context'; import { SettingsTab } from './settings_tab/settings_tab'; import { KnowledgeBaseTab } from './knowledge_base_tab'; @@ -28,6 +29,7 @@ export function SettingsPage() { } = useKibana(); const router = useObservabilityAIAssistantManagementRouter(); + const knowledgeBase = useKnowledgeBase(); const { query: { tab }, @@ -85,6 +87,7 @@ export function SettingsPage() { } ), content: , + disabled: !knowledgeBase.status.value?.enabled, }, { id: 'search_connector', diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx index 2bed5aed37160..02ee9ba06b1ee 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx @@ -9,10 +9,14 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { render } from '../../../helpers/test_helper'; import { SettingsTab } from './settings_tab'; +import { useAppContext } from '../../../hooks/use_app_context'; jest.mock('../../../hooks/use_app_context'); +const useAppContextMock = useAppContext as jest.Mock; + describe('SettingsTab', () => { + useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } }); it('should offer a way to configure Observability AI Assistant visibility in apps', () => { const navigateToAppMock = jest.fn(() => Promise.resolve()); const { getByTestId } = render(, { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx index 71b758f27f580..831ba9ff58054 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; import { UISettings } from './ui_settings'; @@ -15,6 +16,7 @@ export function SettingsTab() { const { application: { navigateToApp }, } = useKibana().services; + const { config } = useAppContext(); const handleNavigateToConnectors = () => { navigateToApp('management', { @@ -30,44 +32,46 @@ export function SettingsTab() { return ( - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel', - { - defaultMessage: - 'Show AI Assistant button and Contextual Insights in Observability apps', - } - )} - - } - description={ -

- {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel', - { - defaultMessage: - 'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > > Features.', - ignoreTag: true, - } - )} -

- } - > - - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel', - { defaultMessage: 'Go to Spaces' } - )} - - -
+ {config.spacesEnabled && ( + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel', + { + defaultMessage: + 'Show AI Assistant button and Contextual Insights in Observability apps', + } + )} + + } + description={ +

+ {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel', + { + defaultMessage: + 'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > > Features.', + ignoreTag: true, + } + )} +

+ } + > + + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel', + { defaultMessage: 'Go to Spaces' } + )} + + +
+ )} ); })} - - + {config.logSourcesEnabled && ( + + )} {!isEmpty(unsavedChanges) && ( ; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { logSourcesEnabled: true, spacesEnabled: true, visibilityEnabled: true }, +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts index 55332dbba35c5..1592b6f4cd72e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +export { config } from './config'; + export const plugin = async () => { const { AiAssistantManagementPlugin } = await import('./plugin'); return new AiAssistantManagementPlugin(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json index 12148ec014725..99bce73e1722f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], "kbn_references": [ "@kbn/core", "@kbn/home-plugin", @@ -22,6 +26,11 @@ "@kbn/config-schema", "@kbn/core-ui-settings-common", "@kbn/logs-data-access-plugin", + "@kbn/core-plugins-browser", + "@kbn/ai-assistant", + "@kbn/core-plugins-server" ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/main_route.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/main_route.tsx index d1a2dc1e74439..49b25c29dad53 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/main_route.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/main_route.tsx @@ -21,26 +21,17 @@ import { } from '../../state_machines/observability_logs_explorer/src'; import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component'; import { ObservabilityLogsExplorerHistory } from '../../types'; -import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs'; +import { useBreadcrumbs } from '../../utils/breadcrumbs'; import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; export const ObservabilityLogsExplorerMainRoute = () => { const { services } = useKibanaContextForPlugin(); - const { - logsExplorer, - serverless, - chrome, - notifications, - appParams, - analytics, - i18n, - theme, - logsDataAccess, - } = services; + const { logsExplorer, notifications, appParams, analytics, i18n, theme, logsDataAccess } = + services; const { history } = appParams; - useBreadcrumbs(noBreadcrumbs, chrome, serverless); + useBreadcrumbs(); const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/utils/breadcrumbs.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/utils/breadcrumbs.tsx index 5e84404239866..4c63476e9507d 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/utils/breadcrumbs.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/utils/breadcrumbs.tsx @@ -5,71 +5,27 @@ * 2.0. */ -import { EuiBreadcrumb } from '@elastic/eui'; -import type { ChromeStart } from '@kbn/core-chrome-browser'; -import { - LOGS_APP_ID, - OBSERVABILITY_LOGS_EXPLORER_APP_ID, - OBSERVABILITY_OVERVIEW_APP_ID, -} from '@kbn/deeplinks-observability'; +import { LOGS_APP_ID, OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import type { ServerlessPluginStart } from '@kbn/serverless/public'; -import { useEffect } from 'react'; -import { - logsExplorerAppTitle, - logsAppTitle, - observabilityAppTitle, -} from '../../common/translations'; +import { useMemo } from 'react'; +import { useBreadcrumbs as observabilityUseBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { logsExplorerAppTitle, logsAppTitle } from '../../common/translations'; -export const useBreadcrumbs = ( - breadcrumbs: EuiBreadcrumb[], - chromeService: ChromeStart, - serverlessService?: ServerlessPluginStart -) => { - const observabilityLinkProps = useLinkProps({ app: OBSERVABILITY_OVERVIEW_APP_ID }); +export const useBreadcrumbs = () => { const logsLinkProps = useLinkProps({ app: LOGS_APP_ID }); const logsExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOGS_EXPLORER_APP_ID }); + const classicCrumbs = useMemo(() => { + return [ + { + text: logsAppTitle, + ...logsLinkProps, + }, + { + text: logsExplorerAppTitle, + ...logsExplorerLinkProps, + }, + ]; + }, [logsExplorerLinkProps, logsLinkProps]); - useEffect(() => { - setBreadcrumbs( - serverlessService - ? breadcrumbs - : [ - { - text: observabilityAppTitle, - ...observabilityLinkProps, - }, - { - text: logsAppTitle, - ...logsLinkProps, - }, - { - text: logsExplorerAppTitle, - ...logsExplorerLinkProps, - }, - ...breadcrumbs, - ], - chromeService, - serverlessService - ); - }, [breadcrumbs, chromeService, serverlessService]); // eslint-disable-line react-hooks/exhaustive-deps + observabilityUseBreadcrumbs(classicCrumbs, { classicOnly: true }); }; - -export function setBreadcrumbs( - breadcrumbs: EuiBreadcrumb[], - chromeService: ChromeStart, - serverlessService?: ServerlessPluginStart -) { - chromeService.docTitle.change(getDocTitle(breadcrumbs)); - if (serverlessService) { - serverlessService.setBreadcrumbs(breadcrumbs); - } else if (chromeService) { - chromeService.setBreadcrumbs(breadcrumbs); - } -} - -export function getDocTitle(breadcrumbs: EuiBreadcrumb[]) { - return breadcrumbs.map(({ text }) => text as string).reverse(); -} - -export const noBreadcrumbs: EuiBreadcrumb[] = []; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json index be2b3c9efdff6..5a2f18aa4249a 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json @@ -13,7 +13,6 @@ "kbn_references": [ "@kbn/config-schema", "@kbn/core", - "@kbn/core-chrome-browser", "@kbn/core-mount-utils-browser-internal", "@kbn/core-notifications-browser", "@kbn/data-plugin", diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx index eb359f6158030..d6a72e25a2a9d 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx @@ -51,7 +51,7 @@ export function useCustomCardsForCategory( title: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectTitle', { - defaultMessage: 'Auto-detect Integrations with Elastic Agent', + defaultMessage: 'Elastic Agent: Logs & Metrics', } ), description: i18n.translate( @@ -79,7 +79,6 @@ export function useCustomCardsForCategory( version: '', integration: '', isQuickstart: true, - release: 'preview', }, { id: 'otel-logs', @@ -88,7 +87,7 @@ export function useCustomCardsForCategory( title: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelTitle', { - defaultMessage: 'Host monitoring with EDOT Collector', + defaultMessage: 'OpenTelemetry: Logs & Metrics', } ), description: i18n.translate( @@ -130,14 +129,13 @@ export function useCustomCardsForCategory( title: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesTitle', { - defaultMessage: 'Kubernetes monitoring with Elastic Agent', + defaultMessage: 'Elastic Agent: Logs & Metrics', } ), description: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesDescription', { - defaultMessage: - 'Monitor your Kubernetes cluster with Elastic Agent, collect container logs', + defaultMessage: 'Collect logs and metrics from Kubernetes using Elastic Agent', } ), extraLabelsBadges: [ @@ -156,7 +154,6 @@ export function useCustomCardsForCategory( version: '', integration: '', isQuickstart: true, - release: 'preview', }, { id: 'otel-kubernetes', @@ -165,14 +162,14 @@ export function useCustomCardsForCategory( title: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelTitle', { - defaultMessage: 'Kubernetes monitoring with EDOT Collector', + defaultMessage: 'OpenTelemetry: Full Observability', } ), description: i18n.translate( 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelDescription', { defaultMessage: - 'Unified Kubernetes observability with Elastic Distro for OTel Collector', + 'Collect logs, traces and metrics with the Elastic Distro for OTel Collector', } ), extraLabelsBadges: [ diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/auto_detect.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/auto_detect.tsx index 7dc3d0acb0a2e..585e1061291a5 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/auto_detect.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/auto_detect.tsx @@ -29,7 +29,6 @@ export const AutoDetectPage = () => ( 'This installation scans your host and auto-detects log and metric files.', } )} - isTechnicalPreview={true} /> } > diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/kubernetes.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/kubernetes.tsx index f92b1d9a83ac6..8e1af954736c1 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/kubernetes.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/kubernetes.tsx @@ -29,7 +29,6 @@ export const KubernetesPage = () => ( 'This installation is tailored for configuring and collecting metrics and logs by deploying a new Elastic Agent within your host.', } )} - isTechnicalPreview={true} /> } > diff --git a/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.test.tsx index 15849039bde8b..c166057df0304 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.test.tsx @@ -11,12 +11,18 @@ import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { useBreadcrumbs } from './use_breadcrumbs'; +import { BehaviorSubject } from 'rxjs'; +import { ChromeStyle } from '@kbn/core-chrome-browser'; const setBreadcrumbs = jest.fn(); const setTitle = jest.fn(); const kibanaServices = { application: { getUrlForApp: () => {}, navigateToApp: () => {} }, - chrome: { setBreadcrumbs, docTitle: { change: setTitle } }, + chrome: { + setBreadcrumbs, + docTitle: { change: setTitle }, + getChromeStyle$: () => new BehaviorSubject('classic').asObservable(), + }, uiSettings: { get: () => true }, settings: { client: { get: () => true } }, } as unknown as Partial; @@ -61,9 +67,15 @@ describe('useBreadcrumbs', () => { it('sets the overview breadcrumb', () => { renderHook(() => useBreadcrumbs([]), { wrapper: Wrapper }); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: '/overview', onClick: expect.any(Function), text: 'Observability' }, - ]); + expect(setBreadcrumbs).toHaveBeenCalledWith( + [{ href: '/overview', onClick: expect.any(Function), text: 'Observability' }], + { + project: { + absolute: true, + value: [{ href: '/overview', onClick: expect.any(Function), text: 'Observability' }], + }, + } + ); }); it('sets the overview title', () => { @@ -86,17 +98,29 @@ describe('useBreadcrumbs', () => { { wrapper: Wrapper } ); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: '/overview', onClick: expect.any(Function), text: 'Observability' }, + expect(setBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/overview', onClick: expect.any(Function), text: 'Observability' }, + { + href: '/one', + onClick: expect.any(Function), + text: 'One', + }, + { + text: 'Two', + }, + ], { - href: '/one', - onClick: expect.any(Function), - text: 'One', - }, - { - text: 'Two', - }, - ]); + project: { + absolute: true, + value: [ + { href: '/overview', onClick: expect.any(Function), text: 'Observability' }, + { href: '/one', onClick: expect.any(Function), text: 'One' }, + { text: 'Two' }, + ], + }, + } + ); }); it('sets the title', () => { diff --git a/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.ts index 4137d541c4e39..5c9c0d3981bb0 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/hooks/use_breadcrumbs.ts @@ -11,8 +11,16 @@ import { MouseEvent, useEffect, useMemo } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import useObservable from 'react-use/lib/useObservable'; import { useQueryParams } from './use_query_params'; +const OBSERVABILITY_TEXT = i18n.translate( + 'xpack.observabilityShared.breadcrumbs.observabilityLinkText', + { + defaultMessage: 'Observability', + } +); + function addClickHandlers( breadcrumbs: ChromeBreadcrumb[], navigateToHref?: (url: string) => Promise @@ -33,23 +41,49 @@ function addClickHandlers( } function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) { - return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse(); + return breadcrumbs + .map(({ text }) => text?.toString() ?? '') + .reverse() + .concat([OBSERVABILITY_TEXT]); } +/** + * + * By default the breadcrumbs will be passed to either serverless.setBreadcrumbs or chrome.setBreadcrumbs depending on the + * environment. The breadcrumbs will *also* be passed to the project style breadcrumbs for stateful project style. We will use "project style" + * here to refer to serverless chrome and stateful project style chrome. Classic refers to stateful classic chrome. + * + * Project style breadcrumbs add a root crumb ("deployment" etc) and "nav crumbs" which are derived from the navigation structure. By default + * the "absolute" mode is used which means the breadcrumbs passed here will omit the navigation derived "nav crumbs". You can pass + * absoluteProjectStyleBreadcrumbs: false to include the 'smart' "nav crumbs". + * + * In classic mode (not project style) the 'Observability' crumb is added. + * + * You can also pass classicOnly to only set breadrumbs in the classic chrome context. This can be useful if your solution just wants to defer all project style crumbs to the "nav crumbs". + */ export const useBreadcrumbs = ( extraCrumbs: ChromeBreadcrumb[], options?: { app?: { id: string; label: string }; breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension; serverless?: ServerlessPluginStart; + absoluteProjectStyleBreadcrumbs?: boolean; + classicOnly?: boolean; } ) => { const params = useQueryParams(); const { app, breadcrumbsAppendExtension, serverless } = options ?? {}; + const absolute = options?.absoluteProjectStyleBreadcrumbs === false ? false : true; + const classicOnly = options?.classicOnly ?? false; const { services: { - chrome: { docTitle, setBreadcrumbs: chromeSetBreadcrumbs, setBreadcrumbsAppendExtension }, + chrome: { + docTitle, + setBreadcrumbs: chromeSetBreadcrumbs, + setBreadcrumbsAppendExtension, + getChromeStyle$, + }, application: { getUrlForApp, navigateToUrl }, }, } = useKibana<{ @@ -58,11 +92,27 @@ export const useBreadcrumbs = ( }>(); const setTitle = docTitle.change; const appPath = getUrlForApp(app?.id ?? 'observability-overview') ?? ''; + const chromeStyle = useObservable(getChromeStyle$()); - const setBreadcrumbs = useMemo( - () => serverless?.setBreadcrumbs ?? chromeSetBreadcrumbs, - [serverless, chromeSetBreadcrumbs] - ); + const setBreadcrumbs = useMemo(() => { + if (!serverless?.setBreadcrumbs) { + return (breadcrumbs: ChromeBreadcrumb[]) => + chromeSetBreadcrumbs( + breadcrumbs, + !classicOnly + ? { + project: { + value: breadcrumbs, + absolute, + }, + } + : undefined + ); + } + if (!classicOnly) + return (breadcrumbs: ChromeBreadcrumb[]) => + serverless?.setBreadcrumbs(breadcrumbs, { absolute }); + }, [serverless, classicOnly, absolute, chromeSetBreadcrumbs]); useEffect(() => { if (breadcrumbsAppendExtension) { @@ -76,15 +126,12 @@ export const useBreadcrumbs = ( }, [breadcrumbsAppendExtension, setBreadcrumbsAppendExtension]); useEffect(() => { - const breadcrumbs = serverless + const isProjectStyle = serverless || chromeStyle === 'project'; + const breadcrumbs = isProjectStyle ? extraCrumbs : [ { - text: - app?.label ?? - i18n.translate('xpack.observabilityShared.breadcrumbs.observabilityLinkText', { - defaultMessage: 'Observability', - }), + text: app?.label ?? OBSERVABILITY_TEXT, href: appPath + '/overview', }, ...extraCrumbs, @@ -94,11 +141,12 @@ export const useBreadcrumbs = ( setBreadcrumbs(addClickHandlers(breadcrumbs, navigateToUrl)); } if (setTitle) { - setTitle(getTitleFromBreadCrumbs(breadcrumbs)); + setTitle(getTitleFromBreadCrumbs(extraCrumbs)); } }, [ app?.label, appPath, + chromeStyle, extraCrumbs, navigateToUrl, params, diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx index 1ca47e02f4df3..f452f77cb1da3 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { Chart, - DARK_THEME, isMetricElementEvent, Metric, MetricTrendShape, @@ -73,12 +72,18 @@ const getSloChartData = ({ }; }; +const ROW_HEIGHT = 220; +const ITEMS_PER_ROW = 4; + export function SloCardChartList({ sloId }: { sloId: string }) { const { http: { basePath }, uiSettings, + charts, } = useKibana().services; + const baseTheme = charts.theme.useChartsBaseTheme(); + const [selectedSlo, setSelectedSlo] = React.useState(null); const kqlQuery = `slo.id:"${sloId}"`; @@ -89,6 +94,7 @@ export function SloCardChartList({ sloId }: { sloId: string }) { const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIdsAndInstanceIds: [[sloId, ALL_VALUE]], + rangeFrom: 'now-24h', }); const { data: rulesBySlo } = useFetchRulesForSlo({ @@ -151,16 +157,24 @@ export function SloCardChartList({ sloId }: { sloId: string }) { ); } + const height = sloList?.results + ? ROW_HEIGHT * Math.ceil(sloList.results.length / ITEMS_PER_ROW) + : ROW_HEIGHT; + return ( <> -
- +
+ { if (isMetricElementEvent(d)) { const { columnIndex, rowIndex } = d; - const slo = sloList?.results[rowIndex * 4 + columnIndex]; + const slo = sloList?.results[rowIndex * ITEMS_PER_ROW + columnIndex]; setSelectedSlo(slo ?? null); } }} diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts index 1f353e6a38558..6ad34d8c4dc86 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts @@ -20,6 +20,7 @@ type SloIdAndInstanceId = [string, string]; interface Params { sloIdsAndInstanceIds: SloIdAndInstanceId[]; shouldRefetch?: boolean; + rangeFrom?: string; } export interface UseFetchActiveAlerts { @@ -46,6 +47,7 @@ const EMPTY_ACTIVE_ALERTS_MAP = new ActiveAlerts(); export function useFetchActiveAlerts({ sloIdsAndInstanceIds = [], shouldRefetch = false, + rangeFrom = 'now-5m/m', }: Params): UseFetchActiveAlerts { const { http } = useKibana().services; @@ -63,7 +65,7 @@ export function useFetchActiveAlerts({ { range: { '@timestamp': { - gte: 'now-5m/m', + gte: rangeFrom, }, }, }, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx index 26cdf62b4f7b4..9a32c150e1b8c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx @@ -40,6 +40,7 @@ export function SloDetailsPage() { application: { navigateToUrl }, http: { basePath }, observabilityAIAssistant, + serverless, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); const { hasAtLeast } = useLicense(); @@ -105,7 +106,7 @@ export function SloDetailsPage() { } }, [onPageReady, slo, isLoading]); - useBreadcrumbs(getBreadcrumbs(basePath, slo)); + useBreadcrumbs(getBreadcrumbs(basePath, slo), { serverless }); const isSloNotFound = !isLoading && slo === undefined; if (isSloNotFound) { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx index aa008838a8b3c..7dcce93c0d003 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx @@ -22,6 +22,7 @@ export function SloEditPage() { const { application: { navigateToUrl }, http: { basePath }, + serverless, } = useKibana().services; const { sloId } = useParams<{ sloId: string | undefined }>(); @@ -32,32 +33,35 @@ export function SloEditPage() { const hasRightLicense = hasAtLeast('platinum'); const { data: slo } = useFetchSloDetails({ sloId }); - useBreadcrumbs([ - { - href: basePath.prepend(paths.slos), - text: i18n.translate('xpack.slo.breadcrumbs.sloLabel', { - defaultMessage: 'SLOs', - }), - deepLinkId: 'slo', - }, - ...(!!slo - ? [ - { - href: basePath.prepend(paths.sloDetails(slo!.id)), - text: slo!.name, - }, - ] - : []), - { - text: slo - ? i18n.translate('xpack.slo.breadcrumbs.sloEditLabel', { - defaultMessage: 'Edit', - }) - : i18n.translate('xpack.slo.breadcrumbs.sloCreateLabel', { - defaultMessage: 'Create', - }), - }, - ]); + useBreadcrumbs( + [ + { + href: basePath.prepend(paths.slos), + text: i18n.translate('xpack.slo.breadcrumbs.sloLabel', { + defaultMessage: 'SLOs', + }), + deepLinkId: 'slo', + }, + ...(!!slo + ? [ + { + href: basePath.prepend(paths.sloDetails(slo!.id)), + text: slo!.name, + }, + ] + : []), + { + text: slo + ? i18n.translate('xpack.slo.breadcrumbs.sloEditLabel', { + defaultMessage: 'Edit', + }) + : i18n.translate('xpack.slo.breadcrumbs.sloCreateLabel', { + defaultMessage: 'Create', + }), + }, + ], + { serverless } + ); useEffect(() => { if (hasRightLicense === false || permissions?.hasAllReadRequested === false) { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx index a9afc480676c8..5a35061b464e5 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx @@ -23,26 +23,30 @@ import { OutdatedSloSearchBar } from './outdated_slo_search_bar'; export function SlosOutdatedDefinitions() { const { http: { basePath }, + serverless, } = useKibana().services; const { data: permissions } = usePermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const { hasAtLeast } = useLicense(); - useBreadcrumbs([ - { - href: basePath.prepend(paths.slos), - text: i18n.translate('xpack.slo.breadcrumbs.slosLinkText', { - defaultMessage: 'SLOs', - }), - deepLinkId: 'slo', - }, - { - text: i18n.translate('xpack.slo.breadcrumbs.slosOutdatedDefinitions', { - defaultMessage: 'Outdated SLO Definitions', - }), - }, - ]); + useBreadcrumbs( + [ + { + href: basePath.prepend(paths.slos), + text: i18n.translate('xpack.slo.breadcrumbs.slosLinkText', { + defaultMessage: 'SLOs', + }), + deepLinkId: 'slo', + }, + { + text: i18n.translate('xpack.slo.breadcrumbs.slosOutdatedDefinitions', { + defaultMessage: 'Outdated SLO Definitions', + }), + }, + ], + { serverless } + ); const [search, setSearch] = useState(''); const [activePage, setActivePage] = useState(0); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx index d2a2da4a5fafa..ca41c7561fb46 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx @@ -16,17 +16,21 @@ import { HeaderMenu } from '../../components/header_menu/header_menu'; export function SloSettingsPage() { const { http: { basePath }, + serverless, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); - useBreadcrumbs([ - { - href: basePath.prepend(paths.slosSettings), - text: i18n.translate('xpack.slo.breadcrumbs.slosSettingsText', { - defaultMessage: 'SLOs Settings', - }), - }, - ]); + useBreadcrumbs( + [ + { + href: basePath.prepend(paths.slosSettings), + text: i18n.translate('xpack.slo.breadcrumbs.slosSettingsText', { + defaultMessage: 'SLOs Settings', + }), + }, + ], + { serverless } + ); return ( 0 ? 2 : 1; + return ( { @@ -57,13 +63,14 @@ export function SloCardItemBadges({ slo, activeAlerts, rules, handleCreateRule } ) : ( <> + { if ((!isLoading && total === 0) || hasAtLeast('platinum') === false || isError) { diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts index 161a58d650e6c..431b3e39fc952 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { journey, step, before, after, expect } from '@elastic/synthetics'; -import { RetryService } from '@kbn/ftr-common-functional-services'; +import { journey, step, before, after } from '@elastic/synthetics'; import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; import { SyntheticsServices } from '../services/synthetics_services'; @@ -14,8 +13,6 @@ journey(`CustomStatusAlert`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params }); const services = new SyntheticsServices(params); - const getService = params.getService; - const retry: RetryService = getService('retry'); const firstCheckTime = new Date(Date.now()).toISOString(); @@ -60,19 +57,7 @@ journey(`CustomStatusAlert`, async ({ page, params }) => { }); step('verify rule creation', async () => { - await retry.try(async () => { - const rules = await services.getRules(); - expect(rules.length).toBe(3); - expect(rules[2].params).toStrictEqual({ - condition: { - downThreshold: 3, - locationsThreshold: 1, - groupBy: 'locationId', - window: { - numberOfChecks: 5, - }, - }, - }); - }); + await syntheticsApp.goToRulesPage(); + await page.waitForSelector(`text='Synthetics status rule'`); }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts index 23c5ef45d1383..5c356492f1c24 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts @@ -221,14 +221,6 @@ export class SyntheticsServices { } } - async getRules() { - const response = await axios.get(this.kibanaUrl + '/internal/alerting/rules/_find', { - auth: { username: 'elastic', password: 'changeme' }, - headers: { 'kbn-xsrf': 'true' }, - }); - return response.data.data; - } - async setupTestConnector() { const indexConnector = { name: 'test index', diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx index f6d5ce45b96c3..931694da554ca 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx @@ -393,5 +393,10 @@ export function syntheticsAppPageProvider({ const isDisabled = await addMonitorBtn.isDisabled(); return !isDisabled; }, + + async goToRulesPage() { + const rulesPage = '/app/observability/alerts/rules'; + await page.goto(basePath + rulesPage); + }, }; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx index 24995e898b3a1..4092f5cd40478 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx @@ -18,7 +18,6 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; import { FormProvider, useForm } from 'react-hook-form'; import { MonitorFilters } from '../monitors_overview/types'; import { MonitorFiltersForm } from './monitor_filters_form'; @@ -29,12 +28,14 @@ interface MonitorConfigurationProps { }; onCreate: (props: { filters: MonitorFilters }) => void; onCancel: () => void; + title: string; } export function MonitorConfiguration({ initialInput, onCreate, onCancel, + title, }: MonitorConfigurationProps) { const methods = useForm({ defaultValues: { @@ -57,64 +58,50 @@ export function MonitorConfiguration({ }; return ( - + - + +

{title}

+
+
+ + - -

- {i18n.translate( - 'xpack.synthetics.overviewEmbeddable.config.sloSelector.headerTitle', - { - defaultMessage: 'Overview configuration', - } - )} -

-
+ + + + + + +
- +
+ + + + + - <> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + +
); } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx index 80d2f3aa072cd..a53d275a12428 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiIconTip } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { FieldSelector } from './field_selector'; @@ -23,18 +23,6 @@ export function MonitorFiltersForm() { )} name="monitorIds" dataTestSubj="syntheticsAvailabilityMonitorSelector" - tooltip={ - - } /> { flyoutSession.close(); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/utils.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/utils.ts new file mode 100644 index 0000000000000..0a5e104a74270 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/utils.ts @@ -0,0 +1,22 @@ +/* + * 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 { MonitorFilters } from '../monitors_overview/types'; + +export const areFiltersEmpty = (filters: MonitorFilters) => { + if (!filters) { + return true; + } + + return ( + !filters.monitorIds?.length && + !filters.projects?.length && + !filters.tags?.length && + !filters.monitorTypes?.length && + !filters.locations?.length + ); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx index e23de3cc67a14..3908063e68116 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx @@ -87,6 +87,12 @@ export const getMonitorsEmbeddableFactory = ( initialState: { filters: filters$.getValue(), }, + title: i18n.translate( + 'xpack.synthetics.editSyntheticsOverviewEmbeddableTitle.overview.title', + { + defaultMessage: 'Create monitors overview', + } + ), }); filters$.next(result.filters); } catch (e) { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx index c3801bb98ca9a..c6465b9d56408 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx @@ -7,8 +7,8 @@ import React, { useEffect, useRef } from 'react'; import { Subject } from 'rxjs'; -import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; +import { areFiltersEmpty } from '../common/utils'; import { getOverviewStore } from './redux_store'; import { ShowSelectedFilters } from '../common/show_selected_filters'; import { setOverviewPageStateAction } from '../../synthetics/state'; @@ -26,13 +26,11 @@ export const StatusGridComponent = ({ }) => { const overviewStore = useRef(getOverviewStore()); + const hasFilters = !areFiltersEmpty(filters); + return ( } + titleAppend={hasFilters ? : null} > diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx index 579596615ae72..a7c1cee656795 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx @@ -8,6 +8,8 @@ import React, { useEffect, useRef } from 'react'; import { Subject } from 'rxjs'; import { useDispatch } from 'react-redux'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { areFiltersEmpty } from '../common/utils'; import { getStatsOverviewStore } from './redux_store'; import { ShowSelectedFilters } from '../common/show_selected_filters'; import { MonitorFilters } from '../monitors_overview/types'; @@ -26,7 +28,16 @@ export const StatsOverviewComponent = ({ return ( - + + + + + ); }; @@ -36,14 +47,21 @@ const WithFiltersComponent = ({ filters }: { filters: MonitorFilters }) => { useEffect(() => { dispatch( setOverviewPageStateAction({ - tags: filters.tags.map((tag) => tag.value), - locations: filters.locations.map((location) => location.value), - monitorTypes: filters.monitorTypes.map((monitorType) => monitorType.value), - monitorQueryIds: filters.monitorIds.map((monitorId) => monitorId.value), - projects: filters.projects.map((project) => project.value), + tags: filters.tags?.map((tag) => tag.value), + locations: filters.locations?.map((location) => location.value), + monitorTypes: filters.monitorTypes?.map((monitorType) => monitorType.value), + monitorQueryIds: filters.monitorIds?.map((monitorId) => monitorId.value), + projects: filters.projects?.map((project) => project.value), }) ); }, [dispatch, filters]); - return } />; + const hasFilters = !areFiltersEmpty(filters); + + return ( + : null} + hideTitle={true} + /> + ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx index 07f1e72fa1e98..83b37f080d422 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx @@ -80,6 +80,9 @@ export const getStatsOverviewEmbeddableFactory = ( initialState: { filters: filters$.getValue(), }, + title: i18n.translate('xpack.synthetics.editSloOverviewEmbeddableTitle.title', { + defaultMessage: 'Create monitor stats', + }), }); filters$.next(result.filters); } catch (e) { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx index 5e7b912d43eba..53e242f77c23c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { createBrowserHistory } from 'history'; -import { EuiPanel } from '@elastic/eui'; import { Router } from '@kbn/shared-ux-router'; import { Subject } from 'rxjs'; import { Store } from 'redux'; @@ -29,14 +28,7 @@ export const SyntheticsEmbeddableContext: React.FC< - - {children} - + {children} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx index c3f935add49c4..1e6eb30c7cc02 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx @@ -38,6 +38,12 @@ export function createMonitorsOverviewPanelAction( const initialState = await openMonitorConfiguration({ coreStart, pluginStart, + title: i18n.translate( + 'xpack.synthetics.editSyntheticsOverviewEmbeddableTitle.overview.title', + { + defaultMessage: 'Create monitors overview', + } + ), }); try { embeddable.addNewPanel({ diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx index 9f88e38c3640c..f3311bec302f7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx @@ -39,6 +39,9 @@ export function createStatusOverviewPanelAction( const initialState = await openMonitorConfiguration({ coreStart, pluginStart, + title: i18n.translate('xpack.synthetics.editSyntheticsOverviewEmbeddableTitle.title', { + defaultMessage: 'Create monitor stats', + }), }); embeddable.addNewPanel({ panelType: SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx index 1d3328197fb55..168984361795e 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx @@ -6,37 +6,58 @@ */ import React, { FC } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiProgress, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { AddToDashboard } from './add_to_dashboard'; import { SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE } from '../../../../embeddables/constants'; -export const EmbeddablePanelWrapper: FC< - React.PropsWithChildren<{ - title: string; - loading?: boolean; - titleAppend?: React.ReactNode; - }> -> = ({ children, title, loading, titleAppend }) => { +interface Props { + title?: string; + loading?: boolean; + hideTitle?: boolean; + titleAppend?: React.ReactNode; +} + +export const EmbeddablePanelWrapper: FC> = ({ + children, + title, + loading, + titleAppend, + hideTitle, +}) => { const isSyntheticsApp = window.location.pathname.includes('/app/synthetics'); + const noTitle = !title && !titleAppend; return ( <> - + + {!noTitle && ( + <> + + + {(!hideTitle || !title) && ( + +

{title}

+
+ )} +
+ {isSyntheticsApp && ( + + + + )} + {titleAppend && {titleAppend}} +
+ + + )} {loading && } - - - -

{title}

-
-
- {isSyntheticsApp && ( - - - - )} - {titleAppend && {titleAppend}} -
- {children}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_items_by_group.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_items_by_group.tsx index 1951e0744c9e8..a2e9226e26162 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_items_by_group.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_items_by_group.tsx @@ -10,6 +10,7 @@ import React, { useRef, useState, FC, PropsWithChildren } from 'react'; import { useSelector } from 'react-redux'; import { get, invert, orderBy } from 'lodash'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { OverviewLoader } from '../overview_loader'; import { getSyntheticsFilterDisplayValues, @@ -60,18 +61,25 @@ export const GridItemsByGroup = ({ items: monitorTypes, values: getSyntheticsFilterDisplayValues(monitorTypes, 'monitorTypes', allLocations), otherValues: { - label: 'Invalid monitor type', + label: i18n.translate('xpack.synthetics.monitorsPage.overview.gridItemsByGroup.noType', { + defaultMessage: 'Invalid monitor type', + }), items: allConfigs?.filter((monitor) => !get(monitor, ConfigKey.MONITOR_TYPE)), }, }; break; case 'locationId': selectedGroup = { - key: 'location.label', + key: 'locationLabel', items: locations, values: getSyntheticsFilterDisplayValues(locations, 'locations', allLocations), otherValues: { - label: 'Without any location', + label: i18n.translate( + 'xpack.synthetics.monitorsPage.overview.gridItemsByGroup.noLocations', + { + defaultMessage: 'Without any location', + } + ), items: allConfigs?.filter((monitor) => !get(monitor, 'location')), }, }; @@ -82,7 +90,9 @@ export const GridItemsByGroup = ({ items: tags, values: getSyntheticsFilterDisplayValues(tags, 'tags', allLocations), otherValues: { - label: 'Without any tags', + label: i18n.translate('xpack.synthetics.monitorsPage.overview.gridItemsByGroup.noTags', { + defaultMessage: 'Without any tags', + }), items: allConfigs?.filter((monitor) => get(monitor, 'tags', []).length === 0), }, }; @@ -93,7 +103,12 @@ export const GridItemsByGroup = ({ items: projects, values: getSyntheticsFilterDisplayValues(projects, 'projects', allLocations), otherValues: { - label: 'UI Monitors', + label: i18n.translate( + 'xpack.synthetics.monitorsPage.overview.gridItemsByGroup.uiMonitors', + { + defaultMessage: 'UI Monitors', + } + ), items: allConfigs?.filter((monitor) => !Boolean(monitor.projectId)), }, }; @@ -138,11 +153,11 @@ export const GridItemsByGroup = ({ ); })} - {selectedGroup.otherValues.items?.length && ( + {(selectedGroup.otherValues.items ?? []).length > 0 && ( - + void; renderGlobalHelpControls(): void; commonlyUsedRanges: CommonlyUsedDateRange[]; - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; appMountParameters: AppMountParameters; isDev: boolean; isServerless: boolean; @@ -89,7 +88,6 @@ export const SyntheticsSettingsContextProvider: React.FC @@ -50,6 +51,9 @@ export const SyntheticsSharedContext: React.FC< coreStart={{ application: coreStart.application, }} + style={{ + height: '100%', + }} > {children} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx index 6a07150070362..d3087d4de061f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { coreMock } from '@kbn/core/public/mocks'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { render } from '../utils/testing'; import React from 'react'; @@ -18,6 +19,8 @@ import { } from '../utils/url_params/get_supported_url_params'; import { makeBaseBreadcrumb, useBreadcrumbs } from './use_breadcrumbs'; import { SyntheticsSettingsContext } from '../contexts'; +import { BehaviorSubject } from 'rxjs'; +import { ChromeStyle } from '@kbn/core-chrome-browser'; describe('useBreadcrumbs', () => { it('sets the given breadcrumbs', () => { @@ -71,9 +74,10 @@ describe('useBreadcrumbs', () => { const urlParams: SyntheticsUrlParams = getSupportedUrlParams({}); expect(JSON.stringify(getBreadcrumbs())).toEqual( JSON.stringify( - makeBaseBreadcrumb('/app/synthetics', '/app/observability', urlParams, false).concat( - expectedCrumbs - ) + [ + { text: 'Observability', href: '/app/observability/overview' }, + ...makeBaseBreadcrumb('/app/synthetics', urlParams), + ].concat(expectedCrumbs) ) ); }); @@ -84,6 +88,8 @@ const mockCore: () => [() => ChromeBreadcrumb[], any] = () => { const get = () => { return breadcrumbObj; }; + const defaultCoreMock = coreMock.createStart(); + const core = { application: { getUrlForApp: (app: string) => @@ -91,6 +97,8 @@ const mockCore: () => [() => ChromeBreadcrumb[], any] = () => { navigateToUrl: jest.fn(), }, chrome: { + ...defaultCoreMock.chrome, + getChromeStyle$: () => new BehaviorSubject('classic').asObservable(), setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { breadcrumbObj = newBreadcrumbs; }, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts index 6d174f773e5a1..c311b08ff22f8 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts @@ -7,47 +7,20 @@ import { ChromeBreadcrumb } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { MouseEvent, useContext, useEffect } from 'react'; +import { useMemo } from 'react'; import { EuiBreadcrumb } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs as useObservabilityBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { ClientPluginsStart } from '../../../plugin'; import { SyntheticsUrlParams, stringifyUrlParams } from '../utils/url_params'; import { useUrlParams } from './use_url_params'; import { PLUGIN } from '../../../../common/constants/plugin'; -import { SyntheticsSettingsContext } from '../contexts'; const EMPTY_QUERY = '?'; -function handleBreadcrumbClick( - breadcrumbs: ChromeBreadcrumb[], - navigateToHref?: (url: string) => Promise -) { - return breadcrumbs.map((bc) => ({ - ...bc, - ...(bc.href - ? { - onClick: (event: MouseEvent) => { - if (navigateToHref && bc.href) { - event.preventDefault(); - navigateToHref(bc.href); - } - }, - } - : {}), - ...(bc['data-test-subj'] - ? { - 'data-test-subj': bc['data-test-subj'], - } - : { - 'data-test-subj': bc.href, - }), - })); -} - export const makeBaseBreadcrumb = ( uptimePath: string, - observabilityPath: string, - params?: SyntheticsUrlParams, - isServerless?: boolean + params?: SyntheticsUrlParams ): EuiBreadcrumb[] => { if (params) { const crumbParams: Partial = { ...params }; @@ -59,18 +32,6 @@ export const makeBaseBreadcrumb = ( const baseBreadcrumbs: EuiBreadcrumb[] = []; - // serverless Kibana has a curated UX flow, and "Observability" is already a given, - // thus we don't need to include it explicitly in the breadcrumb trail - if (!isServerless) { - baseBreadcrumbs.push({ - text: i18n.translate('xpack.synthetics.breadcrumbs.observabilityText', { - defaultMessage: 'Observability', - }), - href: observabilityPath, - 'data-test-subj': 'observabilityPathBreadcrumb', - }); - } - baseBreadcrumbs.push({ text: i18n.translate('xpack.synthetics.breadcrumbs.overviewBreadcrumbText', { defaultMessage: 'Synthetics', @@ -84,32 +45,12 @@ export const makeBaseBreadcrumb = ( export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { const params = useUrlParams()[0](); - const kibana = useKibana(); - const { setBreadcrumbs, isServerless } = useContext(SyntheticsSettingsContext); + const kibana = useKibana(); const syntheticsPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; - const observabilityPath = - kibana.services.application?.getUrlForApp('observability-overview') ?? ''; - const navigate = kibana.services.application?.navigateToUrl; + const breadcrumbs = useMemo(() => { + return makeBaseBreadcrumb(syntheticsPath, params).concat(extraCrumbs); + }, [extraCrumbs, params, syntheticsPath]); - useEffect(() => { - if (setBreadcrumbs) { - setBreadcrumbs( - handleBreadcrumbClick( - makeBaseBreadcrumb(syntheticsPath, observabilityPath, params, isServerless).concat( - extraCrumbs - ), - navigate - ) - ); - } - }, [ - syntheticsPath, - observabilityPath, - extraCrumbs, - navigate, - params, - setBreadcrumbs, - isServerless, - ]); + useObservabilityBreadcrumbs(breadcrumbs, { serverless: kibana.services.serverless }); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/render_app.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/render_app.tsx index 925f39fca7c07..19f97a6e50960 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/render_app.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/render_app.tsx @@ -66,7 +66,6 @@ export const getSyntheticsAppProps = (): SyntheticsAppProps => { setBadge, appMountParameters, isServerless, - setBreadcrumbs: startPlugins.serverless?.setBreadcrumbs ?? coreStart.chrome.setBreadcrumbs, }; }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx index 9d4870b8c9154..1b359a319b332 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx @@ -7,7 +7,7 @@ import React, { ReactElement, ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, @@ -29,6 +29,7 @@ import { KibanaContextProvider, KibanaServices } from '@kbn/kibana-react-plugin/ import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { ChromeStyle } from '@kbn/core-chrome-browser'; import { mockState } from './__mocks__/synthetics_store.mock'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; @@ -166,6 +167,10 @@ export const mockCore: () => Partial = () => {
), }, + chrome: { + ...defaultCore.chrome, + getChromeStyle$: () => new BehaviorSubject('classic').asObservable(), + }, }; return core; diff --git a/x-pack/plugins/observability_solution/synthetics/tsconfig.json b/x-pack/plugins/observability_solution/synthetics/tsconfig.json index 24411ebdcb0c5..5df6d4257b4e9 100644 --- a/x-pack/plugins/observability_solution/synthetics/tsconfig.json +++ b/x-pack/plugins/observability_solution/synthetics/tsconfig.json @@ -104,7 +104,8 @@ "@kbn/babel-register", "@kbn/slo-plugin", "@kbn/ebt-tools", - "@kbn/alerting-types" + "@kbn/alerting-types", + "@kbn/core-chrome-browser" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/ux/public/application/application.test.tsx b/x-pack/plugins/observability_solution/ux/public/application/application.test.tsx index ef9c7df0a1a8d..2b9dd676eac17 100644 --- a/x-pack/plugins/observability_solution/ux/public/application/application.test.tsx +++ b/x-pack/plugins/observability_solution/ux/public/application/application.test.tsx @@ -17,6 +17,8 @@ import { coreMock } from '@kbn/core/public/mocks'; import { merge } from 'lodash'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { BehaviorSubject } from 'rxjs'; +import { ChromeStyle } from '@kbn/core-chrome-browser'; jest.mock('../services/rest/data_view', () => ({ createStaticDataView: () => Promise.resolve(undefined), @@ -122,6 +124,10 @@ const mockCore = merge({}, coreStart, { return uiSettings[key]; }, }, + chrome: { + ...coreStart.chrome, + getChromeStyle$: () => new BehaviorSubject('classic').asObservable(), + }, }); export const mockApmPluginContextValue = { diff --git a/x-pack/plugins/observability_solution/ux/tsconfig.json b/x-pack/plugins/observability_solution/ux/tsconfig.json index 94da70641f150..b27a700aa9b1f 100644 --- a/x-pack/plugins/observability_solution/ux/tsconfig.json +++ b/x-pack/plugins/observability_solution/ux/tsconfig.json @@ -49,7 +49,8 @@ "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", "@kbn/search-types", - "@kbn/server-route-repository-utils" + "@kbn/server-route-repository-utils", + "@kbn/core-chrome-browser" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts index 89881c47083fd..5330b7869e6f4 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts @@ -18,8 +18,7 @@ import { typeInOsqueryFieldInput, } from '../../tasks/live_query'; -// Failing: See https://github.com/elastic/kibana/issues/192128 -describe.skip('EcsMapping', { tags: ['@ess', '@serverless'] }, () => { +describe('EcsMapping', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { initializeDataViews(); }); diff --git a/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts b/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts index 2b0db52f45699..4aa2879883b38 100644 --- a/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts +++ b/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts @@ -231,7 +231,7 @@ export const loadRule = (includeResponseActions = false) => { tags: [], license: '', interval: '1m', - from: 'now-120s', + from: 'now-360s', to: 'now', meta: { from: '1m', kibana_siem_app_url: 'http://localhost:5620/app/security' }, actions: [], diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index c8ef188010130..910427272c5ff 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -58,7 +58,7 @@ export const verifyQueryTimeout = (timeout: string) => { // sometimes the results get stuck in the tests, this is a workaround export const checkResults = () => { - cy.getBySel('osqueryResultsTable').then(($table) => { + cy.getBySel('osqueryResultsTable', { timeout: 120000 }).then(($table) => { if ($table.find('div .euiDataGridRow').length > 0) { cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0); } else { @@ -158,6 +158,7 @@ export const checkActionItemsInResults = ({ cases: boolean; timeline: boolean; }) => { + checkResults(); cy.contains('View in Discover').should(discover ? 'exist' : 'not.exist'); cy.contains('View in Lens').should(lens ? 'exist' : 'not.exist'); cy.contains('Add to Case').should(cases ? 'exist' : 'not.exist'); diff --git a/x-pack/plugins/search_assistant/kibana.jsonc b/x-pack/plugins/search_assistant/kibana.jsonc index 53af40cee6cc6..0f94105943037 100644 --- a/x-pack/plugins/search_assistant/kibana.jsonc +++ b/x-pack/plugins/search_assistant/kibana.jsonc @@ -15,7 +15,6 @@ "actions", "licensing", "observabilityAIAssistant", - "observabilityAIAssistantApp", "triggersActionsUi", "share" ], diff --git a/x-pack/plugins/search_assistant/public/application.tsx b/x-pack/plugins/search_assistant/public/application.tsx deleted file mode 100644 index 1bbf7063ec373..0000000000000 --- a/x-pack/plugins/search_assistant/public/application.tsx +++ /dev/null @@ -1,34 +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 from 'react'; -import ReactDOM from 'react-dom'; -import type { AppMountParameters, CoreStart } from '@kbn/core/public'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { SearchAssistantPluginStartDependencies } from './types'; -import { SearchAssistantRouter } from './components/routes/router'; - -export const renderApp = ( - core: CoreStart, - services: SearchAssistantPluginStartDependencies, - appMountParameters: AppMountParameters -) => { - ReactDOM.render( - - - - - - - , - appMountParameters.element - ); - - return () => ReactDOM.unmountComponentAtNode(appMountParameters.element); -}; diff --git a/x-pack/plugins/search_assistant/public/components/app.tsx b/x-pack/plugins/search_assistant/public/components/app.tsx deleted file mode 100644 index 7d9497c0e1457..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/app.tsx +++ /dev/null @@ -1,15 +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 from 'react'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -export const App: React.FC = () => { - return ( - -
- - ); -}; diff --git a/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx b/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx new file mode 100644 index 0000000000000..a341fdbe81412 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx @@ -0,0 +1,152 @@ +/* + * 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, { useEffect, useRef, useState } from 'react'; +import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; +import { EuiButton, EuiLoadingSpinner, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { v4 } from 'uuid'; +import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@kbn/i18n'; +import { useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant'; +import { useKibana } from '@kbn/ai-assistant/src/hooks/use_kibana'; +import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types'; +import { EuiErrorBoundary } from '@elastic/eui'; +import type { CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; + +interface NavControlWithProviderDeps { + coreStart: CoreStart; + pluginsStart: AIAssistantPluginStartDependencies; +} + +export const NavControlWithProvider = ({ coreStart, pluginsStart }: NavControlWithProviderDeps) => { + return ( + + + + + + + + + + + + ); +}; + +export function NavControl() { + const service = useAIAssistantAppService(); + + const { + services: { notifications, observabilityAIAssistant }, + } = useKibana(); + + const [hasBeenOpened, setHasBeenOpened] = useState(false); + + const chatService = useAbortableAsync( + ({ signal }) => { + return hasBeenOpened + ? service.start({ signal }).catch((error) => { + notifications?.toasts.addError(error, { + title: i18n.translate('xpack.searchAssistant.navControl.initFailureErrorTitle', { + defaultMessage: 'Failed to initialize AI Assistant', + }), + }); + + setHasBeenOpened(false); + setIsOpen(false); + + throw error; + }) + : undefined; + }, + [service, hasBeenOpened, notifications?.toasts] + ); + + const [isOpen, setIsOpen] = useState(false); + + const keyRef = useRef(v4()); + + useEffect(() => { + const conversationSubscription = service.conversations.predefinedConversation$.subscribe(() => { + keyRef.current = v4(); + setHasBeenOpened(true); + setIsOpen(true); + }); + + return () => { + conversationSubscription.unsubscribe(); + }; + }, [service.conversations.predefinedConversation$]); + + const { messages, title } = useObservable(service.conversations.predefinedConversation$) ?? { + messages: [], + title: undefined, + }; + + const theme = useEuiTheme().euiTheme; + + const buttonCss = css` + padding: 0px 8px; + + svg path { + fill: ${theme.colors.darkestShade}; + } + `; + + return ( + <> + + { + service.conversations.openNewConversation({ + messages: [], + }); + }} + color="primary" + size="s" + fullWidth={false} + minWidth={0} + > + {chatService.loading ? : } + + + {chatService.value && + Boolean(observabilityAIAssistant?.ObservabilityAIAssistantChatServiceContext) ? ( + + { + setIsOpen(false); + }} + /> + + ) : undefined} + + ); +} + +const buttonLabel = i18n.translate( + 'xpack.searchAssistant.navControl.openTheAIAssistantPopoverLabel', + { defaultMessage: 'Open the AI Assistant' } +); diff --git a/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx b/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx new file mode 100644 index 0000000000000..d37eea2fae9f4 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx @@ -0,0 +1,26 @@ +/* + * 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 { dynamic } from '@kbn/shared-ux-utility'; +import React from 'react'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; +import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types'; + +const LazyNavControlWithProvider = dynamic(() => + import('.').then((m) => ({ default: m.NavControlWithProvider })) +); + +interface NavControlInitiatorProps { + appService: AIAssistantAppService; + coreStart: CoreStart; + pluginsStart: AIAssistantPluginStartDependencies; +} + +export const NavControlInitiator = ({ coreStart, pluginsStart }: NavControlInitiatorProps) => { + return ; +}; diff --git a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx deleted file mode 100644 index 28ed6d00863f3..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx +++ /dev/null @@ -1,36 +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 from 'react'; -import { ConversationView } from '@kbn/ai-assistant'; -import { useParams } from 'react-router-dom'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; - -export function ConversationViewWithProps() { - const { conversationId } = useParams<{ conversationId?: string }>(); - const { - services: { application, http }, - } = useKibana(); - function navigateToConversation(nextConversationId?: string) { - application?.navigateToUrl( - http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || '' - ); - } - return ( - - http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || '' - } - scopes={['search']} - /> - ); -} diff --git a/x-pack/plugins/search_assistant/public/components/routes/router.tsx b/x-pack/plugins/search_assistant/public/components/routes/router.tsx deleted file mode 100644 index 154bc2ab46a3e..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/routes/router.tsx +++ /dev/null @@ -1,32 +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 from 'react'; -import { History } from 'history'; -import { Route, Router, Routes } from '@kbn/shared-ux-router'; -import { Redirect } from 'react-router-dom'; -import { SearchAIAssistantPageTemplate } from '../page_template'; -import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; - -export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => { - return ( - - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/plugin.ts b/x-pack/plugins/search_assistant/public/plugin.tsx similarity index 51% rename from x-pack/plugins/search_assistant/public/plugin.ts rename to x-pack/plugins/search_assistant/public/plugin.tsx index 1c09502c154ad..15c1443045cdc 100644 --- a/x-pack/plugins/search_assistant/public/plugin.ts +++ b/x-pack/plugins/search_assistant/public/plugin.tsx @@ -5,20 +5,16 @@ * 2.0. */ -import { - DEFAULT_APP_CATEGORIES, - type CoreSetup, - type Plugin, - CoreStart, - AppMountParameters, - PluginInitializerContext, -} from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { createAppService } from '@kbn/ai-assistant'; +import ReactDOM from 'react-dom'; +import React from 'react'; import type { SearchAssistantPluginSetup, SearchAssistantPluginStart, SearchAssistantPluginStartDependencies, } from './types'; +import { NavControlInitiator } from './components/nav_control/lazy_nav_control'; export interface PublicConfigType { ui: { @@ -44,36 +40,43 @@ export class SearchAssistantPlugin public setup( core: CoreSetup ): SearchAssistantPluginSetup { + return {}; + } + + public start( + coreStart: CoreStart, + pluginsStart: SearchAssistantPluginStartDependencies + ): SearchAssistantPluginStart { if (!this.config.ui.enabled) { return {}; } + const appService = createAppService({ + pluginsStart, + }); + const isEnabled = appService.isEnabled(); - core.application.register({ - id: 'searchAssistant', - title: i18n.translate('xpack.searchAssistant.appTitle', { - defaultMessage: 'Search Assistant', - }), - euiIconType: 'logoEnterpriseSearch', - appRoute: '/app/searchAssistant', - category: DEFAULT_APP_CATEGORIES.search, - visibleIn: [], - deepLinks: [], - mount: async (appMountParameters: AppMountParameters) => { - // Load application bundle and Get start services - const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ - import('./application'), - core.getStartServices() as Promise< - [CoreStart, SearchAssistantPluginStartDependencies, unknown] - >, - ]); + if (!isEnabled) { + return {}; + } + + coreStart.chrome.navControls.registerRight({ + mount: (element) => { + ReactDOM.render( + , + element, + () => {} + ); - return renderApp(coreStart, pluginsStart, appMountParameters); + return () => {}; }, + // right before the user profile + order: 1001, }); - return {}; - } - public start(): SearchAssistantPluginStart { return {}; } diff --git a/x-pack/plugins/search_assistant/public/types.ts b/x-pack/plugins/search_assistant/public/types.ts index b1a5d6164b1f1..5b70941d2bf0c 100644 --- a/x-pack/plugins/search_assistant/public/types.ts +++ b/x-pack/plugins/search_assistant/public/types.ts @@ -7,6 +7,10 @@ import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { MlPluginStart } from '@kbn/ml-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchAssistantPluginSetup {} @@ -15,6 +19,10 @@ export interface SearchAssistantPluginSetup {} export interface SearchAssistantPluginStart {} export interface SearchAssistantPluginStartDependencies { + licensing: LicensingPluginStart; + ml: MlPluginStart; observabilityAIAssistant: ObservabilityAIAssistantPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/search_assistant/tsconfig.json b/x-pack/plugins/search_assistant/tsconfig.json index b95020aca1dfc..30002038bbc2d 100644 --- a/x-pack/plugins/search_assistant/tsconfig.json +++ b/x-pack/plugins/search_assistant/tsconfig.json @@ -13,17 +13,22 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/react-kibana-context-render", "@kbn/kibana-react-plugin", - "@kbn/i18n-react", "@kbn/shared-ux-page-kibana-template", "@kbn/usage-collection-plugin", "@kbn/observability-ai-assistant-plugin", "@kbn/config-schema", "@kbn/ai-assistant", "@kbn/i18n", - "@kbn/shared-ux-router", - "@kbn/serverless" + "@kbn/serverless", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-link-redirect-app", + "@kbn/shared-ux-utility", + "@kbn/core-lifecycle-browser", + "@kbn/licensing-plugin", + "@kbn/ml-plugin", + "@kbn/share-plugin", + "@kbn/triggers-actions-ui-plugin" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/search_inference_endpoints/kibana.jsonc b/x-pack/plugins/search_inference_endpoints/kibana.jsonc index e7ba67795f7bf..ce49397901748 100644 --- a/x-pack/plugins/search_inference_endpoints/kibana.jsonc +++ b/x-pack/plugins/search_inference_endpoints/kibana.jsonc @@ -13,12 +13,12 @@ "requiredPlugins": [ "actions", "features", + "ml", "share", ], "optionalPlugins": [ "cloud", "console", - "ml" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx index e13823d87fd8d..54c8713b35336 100644 --- a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx +++ b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx @@ -93,7 +93,9 @@ export const SummarizationModel: React.FC = ({ useEffect(() => { usageTracker?.click( - `${AnalyticsEvents.modelSelected}_${selectedModel!.value || selectedModel!.connectorType}` + `${AnalyticsEvents.modelSelected}_${ + selectedModel?.value || selectedModel?.connectorType || 'unknown' + }` ); }, [usageTracker, selectedModel]); diff --git a/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx b/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx index f2f1f6ff92f79..46b733c535ec4 100644 --- a/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx +++ b/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx @@ -40,10 +40,25 @@ describe('createSessionExpirationToast', () => { }); describe('SessionExpirationToast', () => { - it('renders session expiration time', () => { + it('renders session expiration time in minutes when >= 60s remaining', () => { const sessionState$ = of({ lastExtensionTime: Date.now(), - expiresInMs: 60 * 1000, + expiresInMs: 60 * 2000, + canBeExtended: true, + }); + + const { getByText } = render( + + + + ); + getByText(/You will be logged out in [0-9]+ minutes/); + }); + + it('renders session expiration time in seconds when < 60s remaining', () => { + const sessionState$ = of({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 900, canBeExtended: true, }); diff --git a/x-pack/plugins/security/public/session/session_expiration_toast.tsx b/x-pack/plugins/security/public/session/session_expiration_toast.tsx index de0c460f0f3e1..f38638a77bc33 100644 --- a/x-pack/plugins/security/public/session/session_expiration_toast.tsx +++ b/x-pack/plugins/security/public/session/session_expiration_toast.tsx @@ -44,7 +44,7 @@ export const SessionExpirationToast: FunctionComponent, + timeout: , }} /> ); diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index af8d5901e2b1f..69330c9336bcd 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -94,9 +94,9 @@ export const allowedExperimentalValues = Object.freeze({ endpointManagementSpaceAwarenessEnabled: false, /** - * Enables new notes + * Disables new notes */ - securitySolutionNotesEnabled: false, + securitySolutionNotesDisabled: false, /** * Disables entity and alert previews diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index 931c519ae9b57..5ec3e0c2d0e3d 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -93,8 +93,8 @@ const RowActionComponent = ({ [columnHeaders, timelineNonEcsData] ); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const handleOnEventDetailPanelOpened = useCallback(() => { @@ -175,12 +175,12 @@ const RowActionComponent = ({ showCheckboxes={showCheckboxes} tabType={tabType} timelineId={tableId} - toggleShowNotes={securitySolutionNotesEnabled ? toggleShowNotes : undefined} + toggleShowNotes={securitySolutionNotesDisabled ? undefined : toggleShowNotes} width={width} setEventsLoading={setEventsLoading} setEventsDeleted={setEventsDeleted} refetch={refetch} - showNotes={securitySolutionNotesEnabled ? true : false} + showNotes={!securitySolutionNotesDisabled} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index dd5cbb4131b4c..0a5156bddcc99 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -16,6 +16,9 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { licenseService } from '../../hooks/use_license'; import { mockHistory } from '../../mock/router'; import { DEFAULT_EVENTS_STACK_BY_VALUE } from './histogram_configurations'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; + +jest.mock('../../hooks/use_experimental_features'); const mockGetDefaultControlColumn = jest.fn(); jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ @@ -95,6 +98,7 @@ describe('EventsQueryTabBody', () => { }; beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); jest.clearAllMocks(); }); @@ -106,7 +110,7 @@ describe('EventsQueryTabBody', () => { ); expect(queryByText('MockedStatefulEventsViewer')).toBeInTheDocument(); - expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); }); it('renders the matrix histogram when globalFullScreen is false', () => { @@ -186,7 +190,19 @@ describe('EventsQueryTabBody', () => { expect(spy).toHaveBeenCalled(); }); - it('only have 4 columns on Action bar for non-Enterprise user', () => { + it('should have 5 columns on Action bar for non-Enterprise user', () => { + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); + }); + + it('should have 4 columns on Action bar for non-Enterprise user and securitySolutionNotesDisabled is true', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + render( @@ -196,10 +212,23 @@ describe('EventsQueryTabBody', () => { expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); }); - it('shows 5 columns on Action bar for Enterprise user', () => { + it('should 6 columns on Action bar for Enterprise user', () => { const licenseServiceMock = licenseService as jest.Mocked; + licenseServiceMock.isEnterprise.mockReturnValue(true); + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(6); + }); + + it('should 6 columns on Action bar for Enterprise user and securitySolutionNotesDisabled is true', () => { + const licenseServiceMock = licenseService as jest.Mocked; licenseServiceMock.isEnterprise.mockReturnValue(true); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); render( diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index 92c94549ca891..70e56f4a8b4d2 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -75,10 +75,10 @@ const EventsQueryTabBodyComponent: React.FC = const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const isEnterprisePlus = useLicense().isEnterprise(); let ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); - if (!securitySolutionNotesEnabled) { + if (securitySolutionNotesDisabled) { ACTION_BUTTON_COUNT--; } const leadingControlColumns = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index 2f27b1c8d8d9f..4775b8f889028 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -255,8 +255,8 @@ const ActionsComponent: React.FC = ({ onEventDetailsPanelOpened(); }, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); /* only applicable for new event based notes */ @@ -270,7 +270,7 @@ const ActionsComponent: React.FC = ({ /* note ids associated with the document AND attached to the current timeline, used for pinning */ const timelineNoteIds = useMemo(() => { - if (securitySolutionNotesEnabled) { + if (!securitySolutionNotesDisabled) { // if timeline is unsaved, there is no notes associated to timeline yet return savedObjectId ? documentBasedNotesInTimeline.map((note) => note.noteId) : []; } @@ -280,13 +280,13 @@ const ActionsComponent: React.FC = ({ eventId, documentBasedNotesInTimeline, savedObjectId, - securitySolutionNotesEnabled, + securitySolutionNotesDisabled, ]); /* note count of the document */ const notesCount = useMemo( - () => (securitySolutionNotesEnabled ? documentBasedNotes.length : timelineNoteIds.length), - [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled] + () => (securitySolutionNotesDisabled ? timelineNoteIds.length : documentBasedNotes.length), + [documentBasedNotes, timelineNoteIds, securitySolutionNotesDisabled] ); // we hide the analyzer icon if the data is not available for the resolver diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx index 39b8d7cad3f60..286d377d86f56 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx @@ -47,10 +47,10 @@ export const getUseActionColumnHook = } // we only want to show the note icon if the new notes system feature flag is enabled - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); - if (!securitySolutionNotesEnabled) { + if (securitySolutionNotesDisabled) { ACTION_BUTTON_COUNT--; } diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx index 0e598d6463c5a..91f0c42eab385 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { EntitiesList } from './entities_list'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useQueryToggle } from '../../../common/containers/query_toggle'; @@ -15,6 +15,7 @@ import { useErrorToast } from '../../../common/hooks/use_error_toast'; import type { ListEntitiesResponse } from '../../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; import { useGlobalFilterQuery } from '../../../common/hooks/use_global_filter_query'; import { TestProviders } from '../../../common/mock'; +import { times } from 'lodash/fp'; jest.mock('../../../common/containers/use_global_time'); jest.mock('../../../common/containers/query_toggle'); @@ -22,21 +23,23 @@ jest.mock('./hooks/use_entities_list_query'); jest.mock('../../../common/hooks/use_error_toast'); jest.mock('../../../common/hooks/use_global_filter_query'); -const entityName = 'Entity Name'; +const secondPageTestId = 'pagination-button-1'; +const entityName = 'Entity Name 1'; const responseData: ListEntitiesResponse = { page: 1, per_page: 10, - total: 1, - records: [ - { + total: 20, + records: times( + (index) => ({ '@timestamp': '2021-08-02T14:00:00.000Z', - user: { name: entityName }, + user: { name: `Entity Name ${index}` }, entity: { - name: entityName, + name: `Entity Name ${index}`, source: 'test-index', }, - }, - ], + }), + 10 + ), inspect: undefined, }; @@ -81,7 +84,7 @@ describe('EntitiesList', () => { it('displays the correct number of rows', () => { render(, { wrapper: TestProviders }); - expect(screen.getAllByRole('row')).toHaveLength(2); + expect(screen.getAllByRole('row')).toHaveLength(10 + 1); }); it('calls refetch on time range change', () => { @@ -112,6 +115,21 @@ describe('EntitiesList', () => { ); }); + it('should reset the page when sort order changes ', async () => { + render(, { wrapper: TestProviders }); + + const secondPageButton = screen.getByTestId(secondPageTestId); + fireEvent.click(secondPageButton); + + const columnHeader = screen.getByText('Name'); + fireEvent.click(columnHeader); + + await waitFor(() => { + const firstPageButton = screen.getByTestId('pagination-button-0'); + expect(firstPageButton).toHaveAttribute('aria-current', 'true'); + }); + }); + it('displays error toast when there is an error', () => { const error = new Error('Test error'); mockUseEntitiesListQuery.mockReturnValueOnce({ diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx index aa03e41c553cb..69afa8dd32108 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx @@ -94,6 +94,11 @@ export const EntitiesList: React.FC = () => { inspect: data?.inspect ?? null, }); + // Reset the active page when the search criteria changes + useEffect(() => { + setActivePage(0); + }, [sorting, limit, filter]); + const columns = useEntitiesListColumns(); // Force a refetch when "refresh" button is clicked. @@ -112,7 +117,7 @@ export const EntitiesList: React.FC = () => { return ( > = memo(({ path }) => { const { openLeftPanel } = useExpandableFlyoutApi(); const { eventId, indexName, scopeId, getFieldsData, isPreview } = useDocumentDetailsContext(); const eventKind = getField(getFieldsData('event.kind')); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const [visualizationInFlyoutEnabled] = useUiSetting$( @@ -49,14 +49,14 @@ export const LeftPanel: FC> = memo(({ path }) => { eventKind === EventKind.signal ? [tabs.insightsTab, tabs.investigationTab, tabs.responseTab] : [tabs.insightsTab]; - if (securitySolutionNotesEnabled && !isPreview) { + if (!securitySolutionNotesDisabled && !isPreview) { tabList.push(tabs.notesTab); } if (visualizationInFlyoutEnabled && !isPreview) { return [tabs.visualizeTab, ...tabList]; } return tabList; - }, [eventKind, isPreview, securitySolutionNotesEnabled, visualizationInFlyoutEnabled]); + }, [eventKind, isPreview, securitySolutionNotesDisabled, visualizationInFlyoutEnabled]); const selectedTabId = useMemo(() => { const defaultTab = tabsDisplayed[0].id; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx index 6d72ca9e58dfa..8a8293badb6af 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx @@ -78,7 +78,7 @@ describe('', () => { }); it('should render notes section if experimental flag is enabled', () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); const { getByTestId } = renderHeader(mockContextValue); expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx index f128fada5fb29..931da6e8e57c8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx @@ -40,8 +40,8 @@ export const AlertHeaderTitle = memo(() => { refetchFlyoutData, getFieldsData, } = useDocumentDetailsContext(); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const { isAlert, ruleName, timestamp, ruleId } = useBasicDataFromDetailsData( @@ -98,7 +98,30 @@ export const AlertHeaderTitle = memo(() => { /> )} - {securitySolutionNotesEnabled ? ( + {securitySolutionNotesDisabled ? ( + + + + + + + + + + + + ) : ( { - ) : ( - - - - - - - - - - - )} ); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts index 6ab7979c46086..b5c41d1e66faf 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts @@ -35,8 +35,7 @@ const loginWithoutAccess = (url: string) => { loadPage(url); }; -// Failing: See https://github.com/elastic/kibana/issues/191914 -describe.skip('Artifacts pages', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +describe('Artifacts pages', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { let endpointData: ReturnTypeFromChainable | undefined; before(() => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts index 9da25023f0778..86e07e65e83ae 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { waitForAlertsToPopulate } from '@kbn/test-suites-xpack/security_solution_cypress/cypress/tasks/create_new_rule'; +import { login } from '../../tasks/login'; +import { waitForEndpointListPageToBeLoaded } from '../../tasks/response_console'; import type { PolicyData } from '../../../../../common/endpoint/types'; -import { APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import { closeAllToasts } from '../../tasks/toasts'; import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; -import { login } from '../../tasks/login'; -import { loadPage } from '../../tasks/common'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; import { changeAlertsFilter } from '../../tasks/alerts'; @@ -38,21 +38,33 @@ describe( let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; + let ruleId: string; + let ruleName: string; + beforeEach(() => { + login(); + }); before(() => { - getEndpointIntegrationVersion().then((version) => - createAgentPolicyTask(version, 'automated_response_actions').then((data) => { - indexedPolicy = data; - policy = indexedPolicy.integrationPolicies[0]; - - return enableAllPolicyProtections(policy.id).then(() => { - // Create and enroll a new Endpoint host - return createEndpointHost(policy.policy_ids[0]).then((host) => { - createdHost = host as CreateAndEnrollEndpointHostResponse; + getEndpointIntegrationVersion() + .then((version) => + createAgentPolicyTask(version, 'automated_response_actions').then((data) => { + indexedPolicy = data; + policy = indexedPolicy.integrationPolicies[0]; + + return enableAllPolicyProtections(policy.id).then(() => { + // Create and enroll a new Endpoint host + return createEndpointHost(policy.policy_ids[0]).then((host) => { + createdHost = host as CreateAndEnrollEndpointHostResponse; + }); }); + }) + ) + .then(() => { + loadRule().then((data) => { + ruleId = data.id; + ruleName = data.name; }); - }) - ); + }); }); after(() => { @@ -67,47 +79,29 @@ describe( if (createdHost) { deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] }); } - }); - beforeEach(() => { - login(); + if (ruleId) { + cleanupRule(ruleId); + } }); - describe('From alerts', () => { - let ruleId: string; - let ruleName: string; - - before(() => { - loadRule().then((data) => { - ruleId = data.id; - ruleName = data.name; - }); - }); - - after(() => { - if (ruleId) { - cleanupRule(ruleId); - } - }); - - it('should have generated endpoint and rule', () => { - loadPage(APP_ENDPOINTS_PATH); - cy.contains(createdHost.hostname).should('exist'); + it('should have been called against a created host', () => { + waitForEndpointListPageToBeLoaded(createdHost.hostname); + toggleRuleOffAndOn(ruleName); - toggleRuleOffAndOn(ruleName); + visitRuleAlerts(ruleName); + closeAllToasts(); - visitRuleAlerts(ruleName); - closeAllToasts(); + changeAlertsFilter(`process.name: "agentbeat" and agent.id: "${createdHost.agentId}"`); + waitForAlertsToPopulate(); - changeAlertsFilter(`process.name: "agentbeat" and agent.id: "${createdHost.agentId}"`); - cy.getByTestSubj('expand-event').first().click(); - cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click(); - cy.getByTestSubj('securitySolutionFlyoutResponseTab').click(); + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click(); + cy.getByTestSubj('securitySolutionFlyoutResponseTab').click(); - cy.contains(/isolate is pending|isolate completed successfully/g); - cy.contains(/kill-process is pending|kill-process completed successfully/g); - cy.contains('The action was called with a non-existing event field name: entity_id'); - }); + cy.contains(/isolate is pending|isolate completed successfully/g); + cy.contains(/kill-process is pending|kill-process completed successfully/g); + cy.contains('The action was called with a non-existing event field name: entity_id'); }); } ); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index 76004f91ccb48..4b4e9adef0ec2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -10,6 +10,10 @@ import type { KbnClient } from '@kbn/test'; import pRetry from 'p-retry'; import { kibanaPackageJson } from '@kbn/repo-info'; import type { ToolingLog } from '@kbn/tooling-log'; +import { + RETRYABLE_TRANSIENT_ERRORS, + retryOnError, +} from '../../../../../common/endpoint/data_loaders/utils'; import { fetchFleetLatestAvailableAgentVersion } from '../../../../../common/endpoint/utils/fetch_fleet_version'; import { dump } from '../../../../../scripts/endpoint/common/utils'; import { STARTED_TRANSFORM_STATES } from '../../../../../common/constants'; @@ -158,18 +162,17 @@ const stopTransform = async ( ): Promise => { log.debug(`Stopping transform id: ${transformId}`); - await esClient.transform - .stopTransform({ - transform_id: `${transformId}*`, - force: true, - wait_for_completion: true, - allow_no_match: true, - }) - .catch((e) => { - Error.captureStackTrace(e); - log.verbose(dump(e, 8)); - throw e; - }); + await retryOnError( + () => + esClient.transform.stopTransform({ + transform_id: `${transformId}*`, + force: true, + wait_for_completion: true, + allow_no_match: true, + }), + RETRYABLE_TRANSIENT_ERRORS, + log + ); }; const startTransform = async ( @@ -177,9 +180,14 @@ const startTransform = async ( log: ToolingLog, transformId: string ): Promise => { - const transformsResponse = await esClient.transform.getTransformStats({ - transform_id: `${transformId}*`, - }); + const transformsResponse = await retryOnError( + () => + esClient.transform.getTransformStats({ + transform_id: `${transformId}*`, + }), + RETRYABLE_TRANSIENT_ERRORS, + log + ); log.verbose( `Transform status found for [${transformId}*] returned:\n${dump(transformsResponse)}` @@ -193,7 +201,11 @@ const startTransform = async ( log.debug(`Staring transform id: [${transform.id}]`); - return esClient.transform.startTransform({ transform_id: transform.id }); + return retryOnError( + () => esClient.transform.startTransform({ transform_id: transform.id }), + RETRYABLE_TRANSIENT_ERRORS, + log + ); }) ); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts index 1bd0e5652b442..e6e44e7e5517a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts @@ -55,7 +55,7 @@ export const loadRule = (body = {}, includeResponseActions = true) => tags: [], license: '', interval: '1m', - from: 'now-120s', + from: 'now-360s', to: 'now', meta: { from: '1m', kibana_siem_app_url: 'http://localhost:5620/app/security' }, actions: [], diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index 5d124d1035259..c983368164906 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -34,5 +34,6 @@ "@kbn/security-solution-serverless", "@kbn/dev-utils", "@kbn/spaces-plugin", + "@kbn/test-suites-xpack/security_solution_cypress/cypress", ] } diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index d033fdf491ce8..61cbc1a511c09 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -229,7 +229,8 @@ export const links: LinkItem = { path: NOTES_PATH, skipUrlState: true, hideTimeline: true, - experimentalKey: 'securitySolutionNotesEnabled', + hideWhenExperimentalKey: 'securitySolutionNotesDisabled', + globalSearchDisabled: true, }, ], }; diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index dc8314acc276c..d558b1271dc6b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -88,8 +88,8 @@ const NotesTelemetry = () => ( ); export const ManagementContainer = memo(() => { - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const { @@ -162,7 +162,7 @@ export const ManagementContainer = memo(() => { hasPrivilege={canReadActionsLogManagement} /> - {securitySolutionNotesEnabled && ( + {!securitySolutionNotesDisabled && ( )} diff --git a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts index eb5b23641b80c..7498a6f40c6b6 100644 --- a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts +++ b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts @@ -45,8 +45,8 @@ describe('useFetchNotes', () => { expect(typeof result.current.onLoad).toBe('function'); }); - it('should not dispatch action when securitySolutionNotesEnabled is false', () => { - mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); + it('should not dispatch action when securitySolutionNotesDisabled is true', () => { + mockedUseIsExperimentalFeatureEnabled.mockReturnValue(true); const { result } = renderHook(() => useFetchNotes()); result.current.onLoad([{ _id: '1' }]); @@ -54,7 +54,7 @@ describe('useFetchNotes', () => { }); it('should not dispatch action when events array is empty', () => { - mockedUseIsExperimentalFeatureEnabled.mockReturnValue(true); + mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { result } = renderHook(() => useFetchNotes()); result.current.onLoad([]); @@ -62,7 +62,7 @@ describe('useFetchNotes', () => { }); it('should dispatch fetchNotesByDocumentIds with correct ids when conditions are met', () => { - mockedUseIsExperimentalFeatureEnabled.mockReturnValue(true); + mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { result } = renderHook(() => useFetchNotes()); const events = [{ _id: '1' }, { _id: '2' }, { _id: '3' }]; @@ -74,7 +74,7 @@ describe('useFetchNotes', () => { }); it('should memoize onLoad function', () => { - mockedUseIsExperimentalFeatureEnabled.mockReturnValue(true); + mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { result, rerender } = renderHook(() => useFetchNotes()); const firstOnLoad = result.current.onLoad; @@ -84,7 +84,7 @@ describe('useFetchNotes', () => { expect(firstOnLoad).toBe(secondOnLoad); }); - it('should update onLoad when securitySolutionNotesEnabled changes', () => { + it('should update onLoad when securitySolutionNotesDisabled changes', () => { mockedUseIsExperimentalFeatureEnabled.mockReturnValue(true); const { result, rerender } = renderHook(() => useFetchNotes()); diff --git a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts index 2cf599e76bcc9..cdfa8b7600dfd 100644 --- a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts +++ b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts @@ -22,19 +22,19 @@ export interface UseFetchNotesResult { */ export const useFetchNotes = (): UseFetchNotesResult => { const dispatch = useDispatch(); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const onLoad = useCallback( (events: Array>) => { - if (!securitySolutionNotesEnabled || events.length === 0) return; + if (securitySolutionNotesDisabled || events.length === 0) return; const eventIds: string[] = events .map((event) => event._id) .filter((id) => id != null) as string[]; dispatch(fetchNotesByDocumentIds({ documentIds: eventIds })); }, - [dispatch, securitySolutionNotesEnabled] + [dispatch, securitySolutionNotesDisabled] ); return { onLoad }; diff --git a/x-pack/plugins/security_solution/public/notes/links.ts b/x-pack/plugins/security_solution/public/notes/links.ts index ef6c691b6246a..b09877e200fb9 100644 --- a/x-pack/plugins/security_solution/public/notes/links.ts +++ b/x-pack/plugins/security_solution/public/notes/links.ts @@ -21,5 +21,5 @@ export const links: LinkItem = { }), ], links: [], - experimentalKey: 'securitySolutionNotesEnabled', + hideWhenExperimentalKey: 'securitySolutionNotesDisabled', }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/old_notes.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/old_notes.tsx index 71432267ac0da..09c45d887b373 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/old_notes.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/old_notes.tsx @@ -111,7 +111,7 @@ interface NotesTabContentProps { } /** - * Renders the "old" notes tab content. This should be removed when we remove the securitySolutionNotesEnabled feature flag + * Renders the "old" notes tab content. This should be removed when we remove the securitySolutionNotesDisabled feature flag */ export const OldNotes: React.FC = React.memo(({ timelineId }) => { const dispatch = useDispatch(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index e41d9017d49be..6ece4c96bcd53 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -117,8 +117,8 @@ export const EqlTabContentComponent: React.FC = ({ }); const { openFlyout } = useExpandableFlyoutApi(); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const { @@ -139,7 +139,7 @@ export const EqlTabContentComponent: React.FC = ({ const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && securitySolutionNotesEnabled) { + if (eventId && !securitySolutionNotesDisabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -177,7 +177,7 @@ export const EqlTabContentComponent: React.FC = ({ }, [ openFlyout, - securitySolutionNotesEnabled, + securitySolutionNotesDisabled, selectedPatterns, telemetry, timelineId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 601a31d97fa5c..40d4f939b13fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -253,8 +253,8 @@ const TabsContentComponent: React.FC = ({ selectTimelineESQLSavedSearchId(state, timelineId) ); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const [visualizationInFlyoutEnabled] = useUiSetting$( @@ -320,16 +320,20 @@ const TabsContentComponent: React.FC = ({ } }, [fetchNotes, isTimelineSaved]); - const numberOfNotesNewSystem = useSelector((state: State) => + const notesNewSystem = useSelector((state: State) => selectSortedNotesBySavedObjectId(state, { savedObjectId: timelineSavedObjectId, sort: { field: 'created', direction: 'asc' }, }) ); + const numberOfNotesNewSystem = useMemo( + () => notesNewSystem.length + (isEmpty(timelineDescription) ? 0 : 1), + [notesNewSystem, timelineDescription] + ); const numberOfNotes = useMemo( - () => (securitySolutionNotesEnabled ? numberOfNotesNewSystem.length : numberOfNotesOldSystem), - [numberOfNotesNewSystem, numberOfNotesOldSystem, securitySolutionNotesEnabled] + () => (securitySolutionNotesDisabled ? numberOfNotesOldSystem : numberOfNotesNewSystem), + [numberOfNotesNewSystem, numberOfNotesOldSystem, securitySolutionNotesDisabled] ); const setActiveTab = useCallback( @@ -446,9 +450,7 @@ const TabsContentComponent: React.FC = ({ > {i18n.NOTES_TAB} {showTimeline && numberOfNotes > 0 && timelineType === TimelineTypeEnum.default && ( -
- {numberOfNotes} -
+ {numberOfNotes} )} = ({ {showTimeline && numberOfPinnedEvents > 0 && timelineType === TimelineTypeEnum.default && ( -
- {numberOfPinnedEvents} -
+ {numberOfPinnedEvents} )}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx index 4de3728e2290f..452dc1620b946 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx @@ -70,14 +70,14 @@ const mockGlobalStateWithUnSavedTimeline: State = { describe('NotesTabContentComponent', () => { beforeEach(() => { jest.clearAllMocks(); - (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useUserPrivileges as jest.Mock).mockReturnValue({ kibanaSecuritySolutionsPrivileges: { crud: true }, }); }); it('should show the old note system', () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); const { getByTestId, queryByTestId } = render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx index aad74f15d4f62..58522d32dd55f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx @@ -71,7 +71,7 @@ interface NotesTabContentProps { /** * Renders the notes tab content. - * At this time the component support the old notes system and the new notes system (via the securitySolutionNotesEnabled feature flag). + * At this time the component support the old notes system and the new notes system (via the securitySolutionNotesDisabled feature flag). * The old notes system is deprecated and will be removed in the future. * In both cases, the component fetches the notes for the timeline and renders: * - the timeline description @@ -86,8 +86,8 @@ const NotesTabContentComponent: React.FC = React.memo(({ t const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const canCreateNotes = kibanaSecuritySolutionsPrivileges.crud; - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); @@ -180,7 +180,9 @@ const NotesTabContentComponent: React.FC = React.memo(({ t - {securitySolutionNotesEnabled ? ( + {securitySolutionNotesDisabled ? ( + + ) : ( {timelineDescription} @@ -213,8 +215,6 @@ const NotesTabContentComponent: React.FC = React.memo(({ t - ) : ( - )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 959d6a3b52c3e..8f19e90c77a70 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -146,8 +146,8 @@ export const PinnedTabContentComponent: React.FC = ({ }); const { openFlyout } = useExpandableFlyoutApi(); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const { @@ -168,7 +168,7 @@ export const PinnedTabContentComponent: React.FC = ({ const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && securitySolutionNotesEnabled) { + if (eventId && !securitySolutionNotesDisabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -206,7 +206,7 @@ export const PinnedTabContentComponent: React.FC = ({ }, [ openFlyout, - securitySolutionNotesEnabled, + securitySolutionNotesDisabled, selectedPatterns, telemetry, timelineId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 493fdb4bc603e..70afec0d73135 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -882,12 +882,12 @@ describe('query tab with unified timeline', () => { }); describe('Leading actions - notes', () => { - describe('securitySolutionNotesEnabled = true', () => { + describe('securitySolutionNotesDisabled = false', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'securitySolutionNotesEnabled') { - return true; + if (feature === 'securitySolutionNotesDisabled') { + return false; } return allowedExperimentalValues[feature]; }) @@ -937,12 +937,12 @@ describe('query tab with unified timeline', () => { ); }); - describe('securitySolutionNotesEnabled = false', () => { + describe('securitySolutionNotesDisabled = true', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'securitySolutionNotesEnabled') { - return false; + if (feature === 'securitySolutionNotesDisabled') { + return true; } return allowedExperimentalValues[feature]; }) @@ -1071,12 +1071,12 @@ describe('query tab with unified timeline', () => { }); describe('Leading actions - pin', () => { - describe('securitySolutionNotesEnabled = true', () => { + describe('securitySolutionNotesDisabled = false', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'securitySolutionNotesEnabled') { - return true; + if (feature === 'securitySolutionNotesDisabled') { + return false; } return allowedExperimentalValues[feature]; }) @@ -1155,12 +1155,12 @@ describe('query tab with unified timeline', () => { ); }); - describe('securitySolutionNotesEnabled = false', () => { + describe('securitySolutionNotesDisabled = true', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'securitySolutionNotesEnabled') { - return false; + if (feature === 'securitySolutionNotesDisabled') { + return true; } return allowedExperimentalValues[feature]; }) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 478c13db7de73..5627362247196 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -185,8 +185,8 @@ export const QueryTabContentComponent: React.FC = ({ }); const { openFlyout } = useExpandableFlyoutApi(); - const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( - 'securitySolutionNotesEnabled' + const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesDisabled' ); const { @@ -207,7 +207,7 @@ export const QueryTabContentComponent: React.FC = ({ const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && securitySolutionNotesEnabled) { + if (eventId && !securitySolutionNotesDisabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -245,7 +245,7 @@ export const QueryTabContentComponent: React.FC = ({ }, [ openFlyout, - securitySolutionNotesEnabled, + securitySolutionNotesDisabled, selectedPatterns, telemetry, timelineId, diff --git a/x-pack/plugins/security_solution/server/config.mock.ts b/x-pack/plugins/security_solution/server/config.mock.ts index 1d0d31e9387e2..5fb3dc7b3b48d 100644 --- a/x-pack/plugins/security_solution/server/config.mock.ts +++ b/x-pack/plugins/security_solution/server/config.mock.ts @@ -10,6 +10,7 @@ import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { getDefaultConfigSettings } from '../common/config_settings'; import type { ConfigType } from './config'; +import { duration } from 'moment'; export const createMockConfig = (): ConfigType => { const enableExperimental: Array = ['responseActionUploadEnabled']; @@ -45,6 +46,8 @@ export const createMockConfig = (): ConfigType => { }, }, entityStore: { + frequency: duration('1m'), + syncDelay: duration('5m'), developer: { pipelineDebugMode: false, }, diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 1265aa4c25749..240e452cd44bc 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -176,6 +176,8 @@ export const configSchema = schema.object({ }), }), entityStore: schema.object({ + syncDelay: schema.duration({ defaultValue: '60s' }), + frequency: schema.duration({ defaultValue: '60s' }), developer: schema.object({ pipelineDebugMode: schema.boolean({ defaultValue: false }), }), diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts index cf683283e716d..78916c558ee2d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts @@ -75,6 +75,7 @@ import type { EndpointAuthz } from '../../../common/endpoint/types/authz'; import { createLicenseServiceMock } from '../../../common/license/mocks'; import { createFeatureUsageServiceMock } from '../services/feature_usage/mocks'; import { createProductFeaturesServiceMock } from '../../lib/product_features_service/mocks'; +import type { ConfigType } from '../../config'; /** * Creates a mocked EndpointAppContext. @@ -158,11 +159,15 @@ export const createMockEndpointAppContextServiceSetupContract = }; }; +type CreateMockEndpointAppContextServiceStartContractType = Omit< + DeeplyMockedKeys, + 'config' +> & { config: ConfigType }; // DeeplyMockedKeys doesn't support moment.Duration /** * Creates a mocked input contract for the `EndpointAppContextService#start()` method */ export const createMockEndpointAppContextServiceStartContract = - (): DeeplyMockedKeys => { + (): CreateMockEndpointAppContextServiceStartContractType => { const config = createMockConfig(); const logger = loggingSystemMock.create().get('mock_endpoint_app_context'); @@ -184,7 +189,7 @@ export const createMockEndpointAppContextServiceStartContract = securityMock.createMockAuthenticatedUser({ roles: ['superuser'] }) ); - const startContract: DeeplyMockedKeys = { + const startContract: CreateMockEndpointAppContextServiceStartContractType = { security, config, productFeaturesService: createProductFeaturesServiceMock( diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts index 796932d79b364..8b2e802b17b6d 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts @@ -9,8 +9,6 @@ import type { EngineStatus } from '../../../../common/api/entity_analytics'; export const DEFAULT_LOOKBACK_PERIOD = '24h'; -export const DEFAULT_INTERVAL = '30s'; - export const ENGINE_STATUS: Record, EngineStatus> = { INSTALLING: 'installing', STARTED: 'started', diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts index 1eada1ad4d5fc..8bc4a7c3832d7 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts @@ -15,6 +15,7 @@ import type { SortOrder } from '@elastic/elasticsearch/lib/api/types'; import type { EntityType } from '../../../../common/api/entity_analytics/entity_store/common.gen'; import type { DataViewsService } from '@kbn/data-views-plugin/common'; import type { AppClient } from '../../..'; +import type { EntityStoreConfig } from './types'; describe('EntityStoreDataClient', () => { const mockSavedObjectClient = savedObjectsClientMock.create(); @@ -28,6 +29,7 @@ describe('EntityStoreDataClient', () => { kibanaVersion: '9.0.0', dataViewsService: {} as DataViewsService, appClient: {} as AppClient, + config: {} as EntityStoreConfig, }); const defaultSearchParams = { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index 001fe181185b7..c6254d289bf44 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -52,7 +52,7 @@ import { isPromiseFulfilled, isPromiseRejected, } from './utils'; -import type { EntityRecord } from './types'; +import type { EntityRecord, EntityStoreConfig } from './types'; import { CRITICALITY_VALUES } from '../asset_criticality/constants'; interface EntityStoreClientOpts { @@ -65,6 +65,7 @@ interface EntityStoreClientOpts { kibanaVersion: string; dataViewsService: DataViewsService; appClient: AppClient; + config: EntityStoreConfig; } interface SearchEntitiesParams { @@ -120,7 +121,7 @@ export class EntityStoreDataClient { throw new Error('Task Manager is not available'); } - const { logger } = this.options; + const { logger, config } = this.options; await this.riskScoreDataClient.createRiskScoreLatestIndex(); @@ -151,9 +152,10 @@ export class EntityStoreDataClient { this.options.taskManager, indexPattern, filter, + config, pipelineDebugMode ).catch((error) => { - logger.error(`There was an error during async setup of the Entity Store: ${error}`); + logger.error(`There was an error during async setup of the Entity Store: ${error.message}`); }); return descriptor; @@ -165,6 +167,7 @@ export class EntityStoreDataClient { taskManager: TaskManagerStartContract, indexPattern: string, filter: string, + config: EntityStoreConfig, pipelineDebugMode: boolean ) { const { esClient, logger, namespace, appClient, dataViewsService } = this.options; @@ -175,6 +178,8 @@ export class EntityStoreDataClient { entityType, namespace, fieldHistoryLength, + syncDelay: `${config.syncDelay.asSeconds()}s`, + frequency: `${config.frequency.asSeconds()}s`, }); const { entityManagerDefinition } = unitedDefinition; @@ -327,15 +332,19 @@ export class EntityStoreDataClient { taskManager: TaskManagerStartContract, options = { deleteData: false, deleteEngine: true } ) { - const { namespace, logger, esClient, appClient, dataViewsService } = this.options; + const { namespace, logger, esClient, appClient, dataViewsService, config } = this.options; const { deleteData, deleteEngine } = options; const descriptor = await this.engineClient.maybeGet(entityType); const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService); + + // TODO delete unitedDefinition from this method. we only need the id for deletion const unitedDefinition = getUnitedEntityDefinition({ indexPatterns, entityType, namespace: this.options.namespace, fieldHistoryLength: descriptor?.fieldHistoryLength ?? 10, + syncDelay: `${config.syncDelay.asSeconds()}s`, + frequency: `${config.frequency.asSeconds()}s`, }); const { entityManagerDefinition } = unitedDefinition; logger.info(`In namespace ${namespace}: Deleting entity store for ${entityType}`); @@ -346,7 +355,7 @@ export class EntityStoreDataClient { deleteData, }); } catch (e) { - logger.error(`Error deleting entity definition for ${entityType}: ${e.message}`); + logger.warn(`Error deleting entity definition for ${entityType}: ${e.message}`); } await deleteEntityIndexComponentTemplate({ unitedDefinition, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts index e1c28bc2cc073..3ec84e13aa1db 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts @@ -47,7 +47,7 @@ export const stopEntityEngineRoute = ( return response.ok({ body: { stopped: engine.status === ENGINE_STATUS.STOPPED } }); } catch (e) { - logger.error('Error in StopEntityEngine:', e); + logger.error(`Error in StopEntityEngine: ${e.message}`); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/types.ts index e5f1e6db36bca..b71380b2e0677 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/types.ts @@ -7,6 +7,7 @@ import type { HostEntity, UserEntity } from '../../../../common/api/entity_analytics'; import type { CriticalityValues } from '../asset_criticality/constants'; +import type { EntityAnalyticsConfig } from '../types'; export interface HostEntityRecord extends Omit { asset?: { @@ -24,3 +25,5 @@ export interface UserEntityRecord extends Omit { * It represents the data stored in the entity store index. */ export type EntityRecord = HostEntityRecord | UserEntityRecord; + +export type EntityStoreConfig = EntityAnalyticsConfig['entityStore']; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.test.ts index d9c54e1fcd288..fa443ffa94047 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.test.ts @@ -15,6 +15,8 @@ describe('getUnitedEntityDefinition', () => { namespace: 'test', fieldHistoryLength: 10, indexPatterns, + syncDelay: '1m', + frequency: '1m', }); it('mapping', () => { @@ -172,6 +174,10 @@ describe('getUnitedEntityDefinition', () => { ], "latest": Object { "lookbackPeriod": "24h", + "settings": Object { + "frequency": "1m", + "syncDelay": "1m", + }, "timestampField": "@timestamp", }, "managed": true, @@ -312,6 +318,8 @@ describe('getUnitedEntityDefinition', () => { namespace: 'test', fieldHistoryLength: 10, indexPatterns, + syncDelay: '1m', + frequency: '1m', }); it('mapping', () => { @@ -445,6 +453,10 @@ describe('getUnitedEntityDefinition', () => { ], "latest": Object { "lookbackPeriod": "24h", + "settings": Object { + "frequency": "1m", + "syncDelay": "1m", + }, "timestampField": "@timestamp", }, "managed": true, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts index 32cb52a61d469..ba4963d5fea0a 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts @@ -25,6 +25,8 @@ interface Options { namespace: string; fieldHistoryLength: number; indexPatterns: string[]; + syncDelay: string; + frequency: string; } export const getUnitedEntityDefinition = memoize( @@ -33,6 +35,8 @@ export const getUnitedEntityDefinition = memoize( namespace, fieldHistoryLength, indexPatterns, + syncDelay, + frequency, }: Options): UnitedEntityDefinition => { const unitedDefinition = unitedDefinitionBuilders[entityType](fieldHistoryLength); @@ -47,6 +51,8 @@ export const getUnitedEntityDefinition = memoize( ...unitedDefinition, namespace, indexPatterns, + syncDelay, + frequency, }); } ); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts index c5315c5dca2b0..eced765c75193 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts @@ -7,7 +7,7 @@ import { entityDefinitionSchema, type EntityDefinition } from '@kbn/entities-schema'; import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import type { EntityType } from '../../../../../common/api/entity_analytics/entity_store/common.gen'; -import { DEFAULT_INTERVAL, DEFAULT_LOOKBACK_PERIOD } from '../constants'; +import { DEFAULT_LOOKBACK_PERIOD } from '../constants'; import { buildEntityDefinitionId, getIdentityFieldForEntityType } from '../utils'; import type { FieldRetentionDefinition, @@ -25,6 +25,8 @@ export class UnitedEntityDefinition { entityManagerDefinition: EntityDefinition; fieldRetentionDefinition: FieldRetentionDefinition; indexMappings: MappingTypeMapping; + syncDelay: string; + frequency: string; constructor(opts: { version: string; @@ -32,11 +34,15 @@ export class UnitedEntityDefinition { indexPatterns: string[]; fields: UnitedDefinitionField[]; namespace: string; + syncDelay: string; + frequency: string; }) { this.version = opts.version; this.entityType = opts.entityType; this.indexPatterns = opts.indexPatterns; this.fields = opts.fields; + this.frequency = opts.frequency; + this.syncDelay = opts.syncDelay; this.namespace = opts.namespace; this.entityManagerDefinition = this.toEntityManagerDefinition(); this.fieldRetentionDefinition = this.toFieldRetentionDefinition(); @@ -44,7 +50,7 @@ export class UnitedEntityDefinition { } private toEntityManagerDefinition(): EntityDefinition { - const { entityType, namespace, indexPatterns } = this; + const { entityType, namespace, indexPatterns, syncDelay, frequency } = this; const identityField = getIdentityFieldForEntityType(this.entityType); const metadata = this.fields .filter((field) => field.definition) @@ -61,7 +67,10 @@ export class UnitedEntityDefinition { latest: { timestampField: '@timestamp', lookbackPeriod: DEFAULT_LOOKBACK_PERIOD, - interval: DEFAULT_INTERVAL, + settings: { + syncDelay, + frequency, + }, }, version: this.version, managed: true, diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 0782fa25c71eb..6b0b1b0a005cd 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -212,6 +212,7 @@ export class RequestContextFactory implements IRequestContextFactory { taskManager: startPlugins.taskManager, auditLogger: getAuditLogger(), kibanaVersion: options.kibanaVersion, + config: config.entityAnalytics.entityStore, }); }), }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 06dbe47b0e46c..1f566c69b4fa5 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -43696,7 +43696,6 @@ "xpack.synthetics.badge.readOnly.text": "Lecture seule", "xpack.synthetics.badge.readOnly.tooltip": "Enregistrement impossible", "xpack.synthetics.blocked": "Bloqué", - "xpack.synthetics.breadcrumbs.observabilityText": "Observabilité", "xpack.synthetics.breadcrumbs.overviewBreadcrumbText": "Synthetics", "xpack.synthetics.certificates.heading": "Certificats TLS ({total})", "xpack.synthetics.certificates.loading": "Chargement des certificats...", @@ -44469,7 +44468,6 @@ "xpack.synthetics.overview.status.filters.legend": "Statut du moniteur", "xpack.synthetics.overview.status.filters.pending": "En attente", "xpack.synthetics.overview.status.filters.up": "Opérationnel", - "xpack.synthetics.overview.status.headingText": "Statut actuel", "xpack.synthetics.overview.status.pending.description": "En attente", "xpack.synthetics.overview.status.up.description": "Opérationnel", "xpack.synthetics.overview.SyntheticsHeading": "Moniteurs", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6bd8366f8500e..f656057ebfa66 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -43434,7 +43434,6 @@ "xpack.synthetics.badge.readOnly.text": "読み取り専用", "xpack.synthetics.badge.readOnly.tooltip": "を保存できませんでした", "xpack.synthetics.blocked": "ブロック", - "xpack.synthetics.breadcrumbs.observabilityText": "Observability", "xpack.synthetics.breadcrumbs.overviewBreadcrumbText": "Synthetics", "xpack.synthetics.certificates.heading": "TLS証明書({total})", "xpack.synthetics.certificates.loading": "証明書を読み込んでいます...", @@ -44207,7 +44206,6 @@ "xpack.synthetics.overview.status.filters.legend": "監視ステータス", "xpack.synthetics.overview.status.filters.pending": "保留中", "xpack.synthetics.overview.status.filters.up": "アップ", - "xpack.synthetics.overview.status.headingText": "現在のステータス", "xpack.synthetics.overview.status.pending.description": "保留中", "xpack.synthetics.overview.status.up.description": "アップ", "xpack.synthetics.overview.SyntheticsHeading": "監視", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 91893e7314ad9..dd3c01fa171ca 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -43484,7 +43484,6 @@ "xpack.synthetics.badge.readOnly.text": "只读", "xpack.synthetics.badge.readOnly.tooltip": "无法保存", "xpack.synthetics.blocked": "已阻止", - "xpack.synthetics.breadcrumbs.observabilityText": "Observability", "xpack.synthetics.breadcrumbs.overviewBreadcrumbText": "Synthetics", "xpack.synthetics.certificates.heading": "TLS 证书 ({total})", "xpack.synthetics.certificates.loading": "正在加载证书......", @@ -44257,7 +44256,6 @@ "xpack.synthetics.overview.status.filters.legend": "监测状态", "xpack.synthetics.overview.status.filters.pending": "待处理", "xpack.synthetics.overview.status.filters.up": "运行", - "xpack.synthetics.overview.status.headingText": "当前状态", "xpack.synthetics.overview.status.pending.description": "待处理", "xpack.synthetics.overview.status.up.description": "运行", "xpack.synthetics.overview.SyntheticsHeading": "监测", diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 97d31c42b877a..f36b3394e2a89 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -144,7 +144,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); @@ -459,7 +459,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(5000, async () => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'host-5 - Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'host-5 - Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); @@ -476,7 +476,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(5000, async () => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'pod-0 - Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'pod-0 - Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); @@ -494,7 +494,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(5000, async () => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'container-id-4 - Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'container-id-4 - Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 62d2ab7cae785..4e9778630a535 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res.body).to.eql({ ready: false, model_name: TINY_ELSER.id, + enabled: true, }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/event_rendered_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/event_rendered_view.cy.ts index e1e7e07a3df15..332f32b6f053c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/event_rendered_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/event_rendered_view.cy.ts @@ -33,7 +33,8 @@ import { XY_CHART, } from '../../../screens/shared'; -describe(`Event Rendered View`, { tags: ['@ess', '@serverless'] }, () => { +// skipping as this test is also failing on main (see https://github.com/elastic/security-team/issues/10874) +describe.skip(`Event Rendered View`, { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { login(); createRule(getNewRule()); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts index ab8f5557a5faf..8eac8a410f600 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts @@ -42,6 +42,8 @@ import { DOCUMENT_DETAILS_FLYOUT_TABLE_TAB, DOCUMENT_DETAILS_FLYOUT_FOOTER_ISOLATE_HOST, DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_TITLE, + DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE, + DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE, } from '../../../../screens/expandable_flyout/alert_details_right_panel'; import { closeFlyout, @@ -95,9 +97,8 @@ describe('Alert details expandable flyout right panel', { tags: ['@ess', '@serve cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_TITLE).should('have.text', 'Assignees'); - // TODO uncomment when the securitySolutionNotesEnabled feature flag is removed - // cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE).should('have.text', 'Notes'); - // cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE).should('have.text', '0'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE).should('have.text', 'Notes'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE).should('exist'); cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE) .should('be.visible') diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts index 13a4c73f149de..2d9ec5c79ca40 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts @@ -88,7 +88,7 @@ describe('Timeline notes tab', { tags: ['@ess', '@serverless'] }, () => { it('should be able to delete a note', () => { const deleteNoteContent = 'delete me'; addNotesToTimeline(deleteNoteContent); - cy.get(DELETE_NOTE).last().click(); + cy.get(DELETE_NOTE(0)).click(); cy.get(MODAL_CONFIRMATION_BTN).click(); cy.get(NOTE_DESCRIPTION).last().should('not.have.text', deleteNoteContent); }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts index e84a70cbea621..b972a5edd21b8 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts @@ -55,10 +55,10 @@ export const DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE = getDataTestSubject 'securitySolutionFlyoutHeaderAssignees' ); export const DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE = getDataTestSubjectSelector( - 'securitySolutionFlyoutHeaderAssigneesTitle' + 'securitySolutionFlyoutHeaderNotesTitle' ); export const DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE = getDataTestSubjectSelector( - 'securitySolutionFlyoutHeaderAssigneesValue' + 'securitySolutionFlyoutHeaderNotesAddNoteButton' ); /* Footer */ diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index 96bd20dbd29dc..967e018d50929 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -8,7 +8,7 @@ import type { TimelineFilter } from '../objects/timeline'; import { getDataTestSubjectSelector } from '../helpers/common'; -export const ADD_NOTE_BUTTON = '[data-test-subj="add-note"]'; +export const ADD_NOTE_BUTTON = '[data-test-subj="securitySolutionNotesAddNotesButton"]'; export const ADD_FILTER = '[data-test-subj="timeline-search-or-filter"] [data-test-subj="addFilter"]'; @@ -61,15 +61,13 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto export const ROW_ADD_NOTES_BUTTON = '[data-test-subj="timeline-notes-button-small"]'; -export const ADD_NOTE_CONTAINER = '[data-test-subj="add-note-container"]'; +export const NOTE_CARD_CONTENT = (index: number) => + `[data-test-subj="securitySolutionNotesNotesComment-${index}"]`; -export const RESOLVER_GRAPH_CONTAINER = '[data-test-subj="resolver:graph"]'; +export const NOTE_DESCRIPTION = + '[data-test-subj="securitySolutionNotesTimelineDescriptionComment"]'; -export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]'; - -export const NOTE_DESCRIPTION = '[data-test-subj="note-preview-description"]'; - -export const NOTES_TEXT_AREA = '[data-test-subj="add-a-note"] textarea'; +export const NOTES_TEXT_AREA = '[data-test-subj="euiMarkdownEditorTextArea"]'; export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]'; @@ -81,7 +79,8 @@ export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; export const NOTES_LINK = '[data-test-subj="markdown-link"]'; -export const DELETE_NOTE = '[data-test-subj="delete-note"]'; +export const DELETE_NOTE = (index: number) => + `[data-test-subj="securitySolutionNotesDeleteNotesButton-${index}"]`; export const MARKDOWN_INVESTIGATE_BUTTON = '[data-test-subj="insight-investigate-in-timeline-button"]'; @@ -215,8 +214,6 @@ export const TIMELINE_TITLE = '[data-test-subj="timeline-modal-header-title"]'; export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-modal-title-input"]'; -export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; - export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="actionItem-security-detailsFlyout-cellActions-toggleColumn"]'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts index 60e02152fd4ac..60e7c743bbbbb 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts @@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res.body).to.eql({ ready: false, model_name: TINY_ELSER.id, + enabled: true, }); }); }); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts index 44bdd417330a3..0da7003e7b8dd 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts @@ -64,5 +64,8 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) { keyName ); }, + async expectAIAssistantToExist() { + await testSubjects.existOrFail('AiAssistantAppNavControlButton'); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/observability/infra/infra.ts b/x-pack/test_serverless/functional/test_suites/observability/infra/infra.ts index 30cae6484f78b..b91ae6b09bde2 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/infra/infra.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/infra/infra.ts @@ -57,7 +57,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); @@ -87,7 +87,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'demo-stack-redis-01 - Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'demo-stack-redis-01 - Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); @@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const documentTitle = await browser.getTitle(); expect(documentTitle).to.contain( - 'pod-0 - Infrastructure Inventory - Infrastructure - Observability - Elastic' + 'pod-0 - Infrastructure inventory - Infrastructure - Observability - Elastic' ); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts index 036751ef970da..195790e1b0faf 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts @@ -84,5 +84,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchHomePage.createApiKeyInFlyout('ftr-test-key'); await pageObjects.svlSearchHomePage.closeConnectionDetailsFlyout(); }); + + it('shows the AI assistant', async () => { + await pageObjects.svlSearchHomePage.expectAIAssistantToExist(); + }); }); }