diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_select_options.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_select_options.tsx deleted file mode 100644 index b394a8d29c932..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_select_options.tsx +++ /dev/null @@ -1,39 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { InferenceConnector } from '@kbn/inference-common'; -import React from 'react'; -import { ConnectorIcon } from '../../../../connector_list_button/connector_icon'; - -export interface ConnectorSelectOption { - value: string; - inputDisplay: React.ReactNode; -} - -export const buildConnectorSelectOptions = ( - connectors: InferenceConnector[] -): ConnectorSelectOption[] => - connectors.map((connector) => ({ - value: connector.connectorId, - inputDisplay: ( - - - - - {connector.name} - - ), - })); - -export const getEffectiveConnectorId = ( - displayConnectorId: string | undefined, - options: ConnectorSelectOption[] -): string | undefined => - displayConnectorId && options.some((opt) => opt.value === displayConnectorId) - ? displayConnectorId - : options[0]?.value; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_sub_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_sub_panel.tsx new file mode 100644 index 0000000000000..5e683a48b465a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_sub_panel.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiBadge, EuiSelectable } from '@elastic/eui'; +import type { EuiSelectableOption } from '@elastic/eui'; +import type { InferenceConnector } from '@kbn/inference-common'; +import React, { useCallback, useMemo } from 'react'; +import { ConnectorIcon } from '../../../../connector_list_button/connector_icon'; +import { DEFAULT_MODEL_BADGE_LABEL } from './translations'; + +interface ConnectorSubPanelProps { + connectors: InferenceConnector[]; + resolvedConnectorId: string | undefined; + selectedConnectorId: string | undefined; + onSelect: (connectorId: string) => void; +} + +export const ConnectorSubPanel = ({ + connectors, + resolvedConnectorId, + selectedConnectorId, + onSelect, +}: ConnectorSubPanelProps) => { + const options = useMemo( + () => + connectors.map((connector) => ({ + label: connector.name, + key: connector.connectorId, + checked: connector.connectorId === selectedConnectorId ? ('on' as const) : undefined, + prepend: , + append: + connector.connectorId === resolvedConnectorId ? ( + {DEFAULT_MODEL_BADGE_LABEL} + ) : undefined, + })), + [connectors, selectedConnectorId, resolvedConnectorId] + ); + + const handleChange = useCallback( + (newOptions: EuiSelectableOption[]) => { + const selected = newOptions.find((o) => o.checked === 'on'); + if (selected?.key) onSelect(selected.key); + }, + [onSelect] + ); + + return ( + + {(list) => list} + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_helpers.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_helpers.tsx new file mode 100644 index 0000000000000..a417b5425fe62 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_helpers.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { InferenceConnector } from '@kbn/inference-common'; +import React from 'react'; +import { ConnectorIcon } from '../../../../connector_list_button/connector_icon'; +import { ConnectorSubPanel } from './connector_sub_panel'; +import { MODEL_SELECTION_PANEL_TITLE } from './translations'; + +export function buildConnectorSelectionPanel({ + connectors, + resolvedConnectorId, + selectedConnectorId, + onSelect, +}: { + connectors: InferenceConnector[]; + resolvedConnectorId: string | undefined; + selectedConnectorId: string | undefined; + onSelect: (connectorId: string) => void; +}) { + return { + title: MODEL_SELECTION_PANEL_TITLE, + width: 300, + content: ( + + ), + }; +} + +export function buildConnectorMenuItem({ + connector, + panelId, +}: { + connector: InferenceConnector | undefined; + panelId: number; +}): { name: React.ReactNode; panel: number } { + return { + name: ( + + + + + + {connector?.name ?? '—'} + + + ), + panel: panelId, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_split_button.tsx new file mode 100644 index 0000000000000..60ae116af17d0 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_split_button.tsx @@ -0,0 +1,153 @@ +/* + * 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 { + EuiCallOut, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSplitButton, + useEuiTheme, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { useBoolean } from '@kbn/react-hooks'; +import React, { useCallback, useMemo, useState } from 'react'; +import type { ComponentProps } from 'react'; +import { useModelSettingsUrl } from '../../../../../hooks/use_model_settings_url'; +import { MODEL_SETTINGS_LABEL } from './translations'; + +const modelSettingsMenuName = ( + + {MODEL_SETTINGS_LABEL} + + + + +); + +export interface MenuHelpers { + resetMenu: () => void; + closeMenu: () => void; +} + +interface ContextMenuSplitButtonProps { + primaryLabel: React.ReactNode; + primaryIconType?: string; + onPrimaryClick: () => void; + isPrimaryDisabled?: boolean; + primaryDataTestSubj?: string; + + secondaryAriaLabel: string; + isSecondaryDisabled?: boolean; + secondaryDataTestSubj?: string; + + buildPanels: (helpers: MenuHelpers) => Array>; + + error?: Error; + errorTitle?: string; + + color?: ComponentProps['color']; + isLoading?: boolean; + 'data-test-subj'?: string; +} + +export const ContextMenuSplitButton = ({ + primaryLabel, + primaryIconType, + onPrimaryClick, + isPrimaryDisabled, + primaryDataTestSubj, + secondaryAriaLabel, + isSecondaryDisabled, + secondaryDataTestSubj, + buildPanels, + error, + errorTitle, + color, + isLoading, + 'data-test-subj': dataTestSubj, +}: ContextMenuSplitButtonProps) => { + const { euiTheme } = useEuiTheme(); + const [isOpen, { off: close, toggle }] = useBoolean(false); + const [menuResetKey, setMenuResetKey] = useState(0); + const popoverId = useGeneratedHtmlId({ prefix: 'contextMenuSplitButton' }); + const managementUrl = useModelSettingsUrl(); + + const resetMenu = useCallback(() => setMenuResetKey((k) => k + 1), []); + + const closeMenu = useCallback(() => { + close(); + resetMenu(); + }, [close, resetMenu]); + + const panels = useMemo(() => { + const builtPanels = buildPanels({ resetMenu, closeMenu }); + + const settingsItems = managementUrl + ? [ + { isSeparator: true as const }, + { + name: modelSettingsMenuName, + icon: 'gear' as const, + href: managementUrl, + target: '_blank', + onClick: closeMenu, + }, + ] + : []; + + return builtPanels.map((panel, index) => ({ + ...panel, + id: index, + ...(index === 0 && panel.items && settingsItems.length > 0 + ? { items: [...panel.items, ...settingsItems] } + : {}), + })); + }, [buildPanels, resetMenu, closeMenu, managementUrl]); + + const popoverContent = error ? ( + + ) : ( + + ); + + return ( + + + {primaryLabel} + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx new file mode 100644 index 0000000000000..751be327ae749 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferenceConnector } from '@kbn/inference-common'; +import React, { useCallback, useMemo } from 'react'; +import { + CONNECTOR_LOAD_ERROR, + GENERATE_BUTTON_LABEL, + GENERATE_FEATURES_BUTTON_LABEL, + GENERATE_FEATURES_TOOLTIP, + GENERATE_QUERIES_BUTTON_LABEL, + GENERATE_QUERIES_TOOLTIP, + GENERATE_CONFIG_ARIA_LABEL, +} from './translations'; +import { buildConnectorMenuItem, buildConnectorSelectionPanel } from './context_menu_helpers'; +import type { OnboardingConfig } from './types'; +import { ContextMenuSplitButton } from './context_menu_split_button'; +import type { MenuHelpers } from './context_menu_split_button'; + +interface GenerateSplitButtonProps { + config: OnboardingConfig; + allConnectors: InferenceConnector[]; + connectorError: Error | undefined; + featuresResolvedConnectorId: string | undefined; + queriesResolvedConnectorId: string | undefined; + onConfigChange: (config: OnboardingConfig) => void; + onRun: () => void; + onRunFeaturesOnly: () => void; + onRunQueriesOnly: () => void; + isRunDisabled: boolean; + isConfigDisabled: boolean; +} + +export const GenerateSplitButton = ({ + config, + allConnectors, + connectorError, + featuresResolvedConnectorId, + queriesResolvedConnectorId, + onConfigChange, + onRun, + onRunFeaturesOnly, + onRunQueriesOnly, + isRunDisabled, + isConfigDisabled, +}: GenerateSplitButtonProps) => { + const featuresConnector = useMemo( + () => allConnectors.find((c) => c.connectorId === config.connectors.features), + [allConnectors, config.connectors.features] + ); + const queriesConnector = useMemo( + () => allConnectors.find((c) => c.connectorId === config.connectors.queries), + [allConnectors, config.connectors.queries] + ); + + const onSelectFeaturesConnector = useCallback( + (connectorId: string) => { + onConfigChange({ + ...config, + connectors: { ...config.connectors, features: connectorId }, + }); + }, + [config, onConfigChange] + ); + + const onSelectQueriesConnector = useCallback( + (connectorId: string) => { + onConfigChange({ + ...config, + connectors: { ...config.connectors, queries: connectorId }, + }); + }, + [config, onConfigChange] + ); + + const buildPanels = useCallback( + ({ resetMenu, closeMenu }: MenuHelpers) => [ + { + items: [ + { + name: GENERATE_FEATURES_BUTTON_LABEL, + onClick: () => { + closeMenu(); + onRunFeaturesOnly(); + }, + disabled: isRunDisabled, + toolTipContent: GENERATE_FEATURES_TOOLTIP, + toolTipProps: { position: 'right' as const }, + }, + buildConnectorMenuItem({ connector: featuresConnector, panelId: 1 }), + { isSeparator: true as const }, + { + name: GENERATE_QUERIES_BUTTON_LABEL, + onClick: () => { + closeMenu(); + onRunQueriesOnly(); + }, + disabled: isRunDisabled, + toolTipContent: GENERATE_QUERIES_TOOLTIP, + toolTipProps: { position: 'right' as const }, + }, + buildConnectorMenuItem({ connector: queriesConnector, panelId: 2 }), + ], + }, + buildConnectorSelectionPanel({ + connectors: allConnectors, + resolvedConnectorId: featuresResolvedConnectorId, + selectedConnectorId: config.connectors.features, + onSelect: (connectorId) => { + onSelectFeaturesConnector(connectorId); + resetMenu(); + }, + }), + buildConnectorSelectionPanel({ + connectors: allConnectors, + resolvedConnectorId: queriesResolvedConnectorId, + selectedConnectorId: config.connectors.queries, + onSelect: (connectorId) => { + onSelectQueriesConnector(connectorId); + resetMenu(); + }, + }), + ], + [ + isRunDisabled, + featuresConnector, + queriesConnector, + allConnectors, + featuresResolvedConnectorId, + queriesResolvedConnectorId, + config.connectors.features, + config.connectors.queries, + onSelectFeaturesConnector, + onSelectQueriesConnector, + onRunFeaturesOnly, + onRunQueriesOnly, + ] + ); + + return ( + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_connector_popover.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_connector_popover.tsx deleted file mode 100644 index bf9ac88fd7be2..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_connector_popover.tsx +++ /dev/null @@ -1,126 +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 { - EuiButton, - EuiButtonIcon, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPopover, - EuiPopoverTitle, - EuiSuperSelect, - useGeneratedHtmlId, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import type { InferenceConnector } from '@kbn/inference-common'; -import { useBoolean } from '@kbn/react-hooks'; -import React, { useCallback, useMemo } from 'react'; -import { buildConnectorSelectOptions, getEffectiveConnectorId } from './connector_select_options'; -import { - CONNECTOR_LOAD_ERROR, - INSIGHTS_CONNECTOR_POPOVER_ARIA_LABEL, - INSIGHTS_CONNECTOR_POPOVER_TITLE, - RUN_BUTTON_LABEL, -} from './translations'; - -interface InsightsConnectorPopoverProps { - displayConnectorId: string | undefined; - connectorList: InferenceConnector[]; - connectorError: Error | undefined; - onConnectorChange: (connectorId: string) => void; - onRun: () => void; - isRunDisabled: boolean; -} - -const popoverContentStyle = css` - min-width: 280px; -`; - -export const InsightsConnectorPopover = ({ - displayConnectorId, - connectorList, - connectorError, - onConnectorChange, - onRun, - isRunDisabled, -}: InsightsConnectorPopoverProps) => { - const [isOpen, { off: close, toggle }] = useBoolean(false); - const popoverId = useGeneratedHtmlId({ prefix: 'insightsConnectorPopover' }); - const selectId = useGeneratedHtmlId({ prefix: 'insightsConnectorSelect' }); - - const connectorOptions = useMemo( - () => buildConnectorSelectOptions(connectorList), - [connectorList] - ); - const effectiveConnectorId = useMemo( - () => getEffectiveConnectorId(displayConnectorId, connectorOptions), - [displayConnectorId, connectorOptions] - ); - - const handleRun = useCallback(() => { - close(); - onRun(); - }, [close, onRun]); - - return ( - - } - panelPaddingSize="m" - > - {INSIGHTS_CONNECTOR_POPOVER_TITLE} - - {connectorError ? ( - - - - ) : ( - effectiveConnectorId && - connectorOptions.length > 0 && ( - - - - - - ) - )} - - - {RUN_BUTTON_LABEL} - - - - - ); -}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx new file mode 100644 index 0000000000000..b571f4b269d53 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferenceConnector } from '@kbn/inference-common'; +import React, { useCallback, useMemo } from 'react'; +import { + CONNECTOR_LOAD_ERROR, + DISCOVER_INSIGHTS_BUTTON_LABEL, + DISCOVER_INSIGHTS_CONFIG_ARIA_LABEL, +} from './translations'; +import { buildConnectorMenuItem, buildConnectorSelectionPanel } from './context_menu_helpers'; +import { ContextMenuSplitButton } from './context_menu_split_button'; +import type { MenuHelpers } from './context_menu_split_button'; + +interface InsightsSplitButtonProps { + allConnectors: InferenceConnector[]; + connectorError: Error | undefined; + resolvedConnectorId: string | undefined; + displayConnectorId: string | undefined; + onConnectorChange: (connectorId: string) => void; + onRun: () => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const InsightsSplitButton = ({ + allConnectors, + connectorError, + resolvedConnectorId, + displayConnectorId, + onConnectorChange, + onRun, + isLoading, + isDisabled, +}: InsightsSplitButtonProps) => { + const discoveryConnector = useMemo( + () => allConnectors.find((c) => c.connectorId === displayConnectorId), + [allConnectors, displayConnectorId] + ); + + const buildPanels = useCallback( + ({ resetMenu }: MenuHelpers) => [ + { + items: [buildConnectorMenuItem({ connector: discoveryConnector, panelId: 1 })], + }, + buildConnectorSelectionPanel({ + connectors: allConnectors, + resolvedConnectorId, + selectedConnectorId: displayConnectorId, + onSelect: (connectorId) => { + onConnectorChange(connectorId); + resetMenu(); + }, + }), + ], + [discoveryConnector, allConnectors, resolvedConnectorId, displayConnectorId, onConnectorChange] + ); + + return ( + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/onboarding_config_popover.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/onboarding_config_popover.tsx deleted file mode 100644 index 57ad87b53e823..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/onboarding_config_popover.tsx +++ /dev/null @@ -1,218 +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 { - EuiButton, - EuiButtonIcon, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPopover, - EuiPopoverTitle, - EuiSuperSelect, - EuiSwitch, - useGeneratedHtmlId, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import type { InferenceConnector } from '@kbn/inference-common'; -import { useBoolean } from '@kbn/react-hooks'; -import { OnboardingStep } from '@kbn/streams-schema'; -import React, { useCallback, useMemo } from 'react'; -import { buildConnectorSelectOptions, getEffectiveConnectorId } from './connector_select_options'; -import { - CONNECTOR_LOAD_ERROR, - FEATURES_STEP_LABEL, - ONBOARDING_CONFIG_POPOVER_ARIA_LABEL, - ONBOARDING_CONFIG_POPOVER_TITLE, - QUERIES_STEP_LABEL, - RUN_BUTTON_LABEL, -} from './translations'; - -export interface OnboardingConfig { - steps: OnboardingStep[]; - connectors: { - features?: string; - queries?: string; - }; -} - -const STEP_CONNECTOR_KEY: Record = { - [OnboardingStep.FeaturesIdentification]: 'features', - [OnboardingStep.QueriesGeneration]: 'queries', -}; - -const STEPS: ReadonlyArray<{ step: OnboardingStep; label: string }> = [ - { step: OnboardingStep.FeaturesIdentification, label: FEATURES_STEP_LABEL }, - { step: OnboardingStep.QueriesGeneration, label: QUERIES_STEP_LABEL }, -]; - -interface StepRowProps { - step: OnboardingStep; - label: string; - enabled: boolean; - displayConnectorId: string | undefined; - connectorList: InferenceConnector[]; - connectorError: Error | undefined; - onToggle: (step: OnboardingStep, enabled: boolean) => void; - onConnectorChange: (step: OnboardingStep, connectorId: string) => void; -} - -const StepRow = ({ - step, - label, - enabled, - displayConnectorId, - connectorList, - connectorError, - onToggle, - onConnectorChange, -}: StepRowProps) => { - const selectId = useGeneratedHtmlId({ prefix: `onboardingStep_${step}` }); - - const connectorOptions = useMemo( - () => buildConnectorSelectOptions(connectorList), - [connectorList] - ); - const effectiveConnectorId = useMemo( - () => getEffectiveConnectorId(displayConnectorId, connectorOptions), - [displayConnectorId, connectorOptions] - ); - - return ( - - - onToggle(step, e.target.checked)} - compressed - /> - - {connectorError ? ( - - - - ) : ( - effectiveConnectorId && - connectorOptions.length > 0 && ( - - - onConnectorChange(step, value)} - disabled={!enabled} - compressed - fullWidth - /> - - - ) - )} - - ); -}; - -interface OnboardingConfigPopoverProps { - config: OnboardingConfig; - displayConnectors: OnboardingConfig['connectors']; - connectorList: InferenceConnector[]; - connectorError: Error | undefined; - onConfigChange: (config: OnboardingConfig) => void; - onRun: () => void; - isRunDisabled: boolean; -} - -const popoverContentStyle = css` - min-width: 280px; -`; - -export const OnboardingConfigPopover = ({ - config, - displayConnectors, - connectorList, - connectorError, - onConfigChange, - onRun, - isRunDisabled, -}: OnboardingConfigPopoverProps) => { - const [isOpen, { off: close, toggle }] = useBoolean(false); - const popoverId = useGeneratedHtmlId({ prefix: 'onboardingConfigPopover' }); - - const handleToggle = useCallback( - (step: OnboardingStep, enabled: boolean) => { - const toggled = enabled ? [...config.steps, step] : config.steps.filter((s) => s !== step); - const ordered = STEPS.map((s) => s.step).filter((s) => toggled.includes(s)); - onConfigChange({ ...config, steps: ordered }); - }, - [config, onConfigChange] - ); - - const handleConnectorChange = useCallback( - (step: OnboardingStep, newConnectorId: string) => { - const key = STEP_CONNECTOR_KEY[step]; - onConfigChange({ ...config, connectors: { ...config.connectors, [key]: newConnectorId } }); - }, - [config, onConfigChange] - ); - - const handleRun = useCallback(() => { - close(); - onRun(); - }, [close, onRun]); - - return ( - - } - panelPaddingSize="m" - > - {ONBOARDING_CONFIG_POPOVER_TITLE} - - {STEPS.map(({ step, label }) => ( - - - - ))} - - - {RUN_BUTTON_LABEL} - - - - - ); -}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index ee6acdcec34ad..1f6dcc9bce77e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -6,19 +6,12 @@ */ import type { EuiSearchBarProps, Query } from '@elastic/eui'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiSearchBar, - EuiText, -} from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSearchBar, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; -import { TaskStatus } from '@kbn/streams-schema'; +import { OnboardingStep, TaskStatus } from '@kbn/streams-schema'; import pMap from 'p-map'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; @@ -34,10 +27,9 @@ import { useTaskPolling } from '../../../../../hooks/use_task_polling'; import { getFormattedError } from '../../../../../util/errors'; import { StreamsAppSearchBar } from '../../../../streams_app_search_bar'; import { useOnboardingStatusUpdateQueue } from '../../hooks/use_onboarding_status_update_queue'; -import { InsightsConnectorPopover } from './insights_connector_popover'; -import { OnboardingConfigPopover } from './onboarding_config_popover'; +import { GenerateSplitButton } from './generate_split_button'; +import { InsightsSplitButton } from './insights_split_button'; import { - DISCOVER_INSIGHTS_BUTTON_LABEL, getInsightsCompleteToastTitle, INSIGHTS_COMPLETE_TOAST_VIEW_BUTTON, INSIGHTS_SCHEDULING_FAILURE_TITLE, @@ -49,10 +41,6 @@ import { import { StreamsTreeTable } from './tree_table'; import { useFetchStreams } from '../../hooks/use_fetch_streams'; -const onboardingButtonStyle = css` - min-width: 160px; -`; - const datePickerStyle = css` .euiFormControlLayout, .euiSuperDatePicker button, @@ -81,15 +69,14 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { featuresConnectors, queriesConnectors, discoveryConnectors, - genAiConnectors, + allConnectors, + connectorError, isConnectorCatalogUnavailable, discoveryConnectorOverride, setDiscoveryConnectorOverride, displayDiscoveryConnectorId, onboardingConfig, setOnboardingConfig, - displayConnectors, - dynamicButtonLabel, } = useConnectorConfig(); const streamsListFetch = useFetchStreams({ @@ -237,44 +224,72 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { }); }, [onboardingStatusUpdateQueue, processStatusUpdateQueue, streamsListFetch.data]); - const isStreamActionable = (streamName: string) => { - const result = streamOnboardingResultMap[streamName]; - if (!result) return false; - return ![TaskStatus.InProgress, TaskStatus.BeingCanceled].includes(result.status); - }; + const isStreamActionable = useCallback( + (streamName: string) => { + const result = streamOnboardingResultMap[streamName]; + if (!result) return false; + return ![TaskStatus.InProgress, TaskStatus.BeingCanceled].includes(result.status); + }, + [streamOnboardingResultMap] + ); - const getActionableStreamNames = () => - selectedStreams - .filter((item) => isStreamActionable(item.stream.name)) - .map((item) => item.stream.name); + const getActionableStreamNames = useCallback( + () => + selectedStreams + .filter((item) => isStreamActionable(item.stream.name)) + .map((item) => item.stream.name), + [selectedStreams, isStreamActionable] + ); - const bulkScheduleOnboardingTask = async ( - streamList: string[], - options?: ScheduleOnboardingOptions - ) => { - try { - await pMap( - streamList, - async (streamName) => { - await scheduleOnboardingTask(streamName, options); - }, - { concurrency: 10 } - ); - } catch (error) { - toasts.addError(getFormattedError(error), { title: ONBOARDING_SCHEDULING_FAILURE_TITLE }); - } + const bulkScheduleOnboardingTask = useCallback( + async (streamList: string[], options?: ScheduleOnboardingOptions) => { + try { + await pMap( + streamList, + async (streamName) => { + await scheduleOnboardingTask(streamName, options); + }, + { concurrency: 10 } + ); + } catch (error) { + toasts.addError(getFormattedError(error), { title: ONBOARDING_SCHEDULING_FAILURE_TITLE }); + } - streamList.forEach((streamName) => { - onboardingStatusUpdateQueue.add(streamName); - }); - processStatusUpdateQueue(); - }; + streamList.forEach((streamName) => { + onboardingStatusUpdateQueue.add(streamName); + }); + processStatusUpdateQueue(); + }, + [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] + ); - const onBulkOnboardStreamsClick = async () => { + const onBulkOnboardStreamsClick = useCallback(async () => { const streamList = getActionableStreamNames(); setSelectedStreams([]); await bulkScheduleOnboardingTask(streamList, onboardingConfig); - }; + }, [getActionableStreamNames, bulkScheduleOnboardingTask, onboardingConfig]); + + const onBulkOnboardStep = useCallback( + async (step: OnboardingStep) => { + const streamList = getActionableStreamNames(); + setSelectedStreams([]); + await bulkScheduleOnboardingTask(streamList, { + steps: [step], + connectors: onboardingConfig.connectors, + }); + }, + [getActionableStreamNames, bulkScheduleOnboardingTask, onboardingConfig.connectors] + ); + + const onBulkOnboardFeaturesOnly = useCallback( + () => onBulkOnboardStep(OnboardingStep.FeaturesIdentification), + [onBulkOnboardStep] + ); + + const onBulkOnboardQueriesOnly = useCallback( + () => onBulkOnboardStep(OnboardingStep.QueriesGeneration), + [onBulkOnboardStep] + ); const onOnboardStreamActionClick = async (streamName: string) => { await bulkScheduleOnboardingTask([streamName]); @@ -301,88 +316,53 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { - - - - - - - {i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsTree.streamsCountLabel', - { - defaultMessage: '{count} streams', - values: { count: streamsListFetch.data?.streams.length ?? 0 }, - } - )} - - - - - - {dynamicButtonLabel} - - - - - - + - - - - scheduleInsightsTask()} - disabled={isConnectorCatalogUnavailable} - isLoading={isSchedulingInsights || isWaitingForInsightsTask} - data-test-subj="significant_events_discover_insights_button" - size="xs" - > - {DISCOVER_INSIGHTS_BUTTON_LABEL} - - - - - - + + + + {i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsTree.streamsCountLabel', + { + defaultMessage: '{count} streams', + values: { count: streamsListFetch.data?.streams.length ?? 0 }, + } + )} + + + ({ - features: onboardingConfig.connectors.features ?? featuresConnectors.resolvedConnectorId, - queries: onboardingConfig.connectors.queries ?? queriesConnectors.resolvedConnectorId, - }), - [ - onboardingConfig.connectors, - featuresConnectors.resolvedConnectorId, - queriesConnectors.resolvedConnectorId, - ] - ); - - const { steps: selectedSteps } = onboardingConfig; - const dynamicButtonLabel = useMemo(() => { - const hasFeatures = selectedSteps.includes(OnboardingStep.FeaturesIdentification); - const hasQueries = selectedSteps.includes(OnboardingStep.QueriesGeneration); - if (hasFeatures && !hasQueries) return GENERATE_FEATURES_BUTTON_LABEL; - if (hasQueries && !hasFeatures) return GENERATE_QUERIES_BUTTON_LABEL; - return RUN_BULK_STREAM_ONBOARDING_BUTTON_LABEL; - }, [selectedSteps]); + useEffect(() => { + setOnboardingConfig((prev) => { + const features = prev.connectors.features ?? featuresConnectors.resolvedConnectorId; + const queries = prev.connectors.queries ?? queriesConnectors.resolvedConnectorId; + if (features === prev.connectors.features && queries === prev.connectors.queries) { + return prev; + } + return { ...prev, connectors: { features, queries } }; + }); + }, [featuresConnectors.resolvedConnectorId, queriesConnectors.resolvedConnectorId]); return { featuresConnectors, queriesConnectors, discoveryConnectors, - genAiConnectors, + allConnectors, + connectorError, isConnectorCatalogUnavailable, discoveryConnectorOverride, setDiscoveryConnectorOverride, displayDiscoveryConnectorId, onboardingConfig, setOnboardingConfig, - displayConnectors, - dynamicButtonLabel, }; }