From d2fa60ae1d7bd1afe46adfac6fe23917c10e5b9c Mon Sep 17 00:00:00 2001 From: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:50:52 +0100 Subject: [PATCH] [ML] Anomaly Detection: Show `Switch to apply time range` when opening job selector from left nav (#213382) Fix for: https://github.com/elastic/kibana/issues/211018 and https://github.com/elastic/kibana/issues/212407 Note: Previously, the `apply time range` setting was saved in local storage even if the changes were not applied. After the fix, the setting is saved in local storage only if the user applies the new selection. After: https://github.com/user-attachments/assets/1657f0f4-c580-4941-9582-bf5f9dc3cd55 (cherry picked from commit cbcb7edb94e44bc8edc1a4e6f21bff5504c4d61a) --- .../components/job_selector/job_selector.tsx | 93 ++++++------------- .../job_selector/job_selector_flyout.tsx | 17 ++-- .../contexts/ml/use_job_selection_flyout.tsx | 14 ++- 3 files changed, 50 insertions(+), 74 deletions(-) diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx index a01157b362c41..fd26e2baa2fab 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx @@ -5,33 +5,26 @@ * 2.0. */ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; - -import { - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, - EuiFlyout, - EuiHorizontalRule, -} from '@elastic/eui'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; + +import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import './_index.scss'; -import { useStorage } from '@kbn/ml-local-storage'; import { ML_PAGES } from '../../../locator'; import type { Dictionary } from '../../../../common/types/common'; import { IdBadges } from './id_badges'; -import type { JobSelectorFlyoutProps } from './job_selector_flyout'; -import { BADGE_LIMIT, JobSelectorFlyoutContent } from './job_selector_flyout'; + +import { BADGE_LIMIT } from './job_selector_flyout'; import type { MlJobWithTimeRange, MlSummaryJob, } from '../../../../common/types/anomaly_detection_jobs'; -import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage'; import { FeedBackButton } from '../feedback_button'; import { JobInfoFlyoutsProvider } from '../../jobs/components/job_details_flyout'; import { JobInfoFlyoutsManager } from '../../jobs/components/job_details_flyout/job_details_context_manager'; +import { useJobSelectionFlyout } from '../../contexts/ml/use_job_selection_flyout'; export interface GroupObj { groupId: string; @@ -108,17 +101,13 @@ export function JobSelector({ selectedJobs = [], onSelectionChange, }: JobSelectorProps) { - const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useStorage( - ML_APPLY_TIME_RANGE_CONFIG, - true - ); - const [selectedIds, setSelectedIds] = useState( mergeSelection(selectedJobIds, selectedGroups, singleSelection) ); const [showAllBarBadges, setShowAllBarBadges] = useState(false); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + const openJobSelectionFlyout = useJobSelectionFlyout(); // Ensure JobSelectionBar gets updated when selection via globalState changes. useEffect(() => { @@ -126,27 +115,24 @@ export function JobSelector({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify([selectedJobIds, selectedGroups])]); - function closeFlyout() { - setIsFlyoutVisible(false); - } - - function showFlyout() { - setIsFlyoutVisible(true); - } - - function handleJobSelectionClick() { - showFlyout(); - } - - const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback( - ({ newSelection, jobIds, time }) => { - setSelectedIds(newSelection); - - onSelectionChange?.({ jobIds, time }); - closeFlyout(); - }, - [onSelectionChange] - ); + const handleJobSelectionClick = useCallback(async () => { + try { + const result = await openJobSelectionFlyout({ + singleSelection, + withTimeRangeSelector: true, + timeseriesOnly, + selectedIds, + }); + + if (result) { + const { newSelection, jobIds, time } = result; + setSelectedIds(newSelection); + onSelectionChange?.({ jobIds, time }); + } + } catch { + // Flyout closed without selection + } + }, [onSelectionChange, openJobSelectionFlyout, selectedIds, singleSelection, timeseriesOnly]); const page = useMemo(() => { return singleSelection ? ML_PAGES.SINGLE_METRIC_VIEWER : ML_PAGES.ANOMALY_EXPLORER; @@ -154,7 +140,8 @@ export function JobSelector({ const removeJobId = (jobOrGroupId: string[]) => { const newSelection = selectedIds.filter((id) => !jobOrGroupId.includes(id)); - applySelection({ newSelection, jobIds: newSelection, time: undefined }); + setSelectedIds(newSelection); + onSelectionChange?.({ jobIds: newSelection, time: undefined }); }; function renderJobSelectionBar() { return ( @@ -213,34 +200,10 @@ export function JobSelector({ ); } - function renderFlyout() { - if (isFlyoutVisible) { - return ( - - - - ); - } - } - return (
{renderJobSelectionBar()} - {renderFlyout()}
diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx index 58948161af298..0fec01c82c0a1 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -73,7 +73,7 @@ export const JobSelectorFlyoutContent: FC = ({ onJobsFetched, onSelectionConfirmed, onFlyoutClose, - applyTimeRangeConfig, + applyTimeRangeConfig: initialApplyTimeRangeConfig, onTimeRangeConfigChange, withTimeRangeSelector = true, }) => { @@ -85,6 +85,9 @@ export const JobSelectorFlyoutContent: FC = ({ } = useMlKibana(); const [newSelection, setNewSelection] = useState(selectedIds); + const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useState( + initialApplyTimeRangeConfig ?? false + ); const [isLoading, setIsLoading] = useState(true); const [showAllBadges, setShowAllBadges] = useState(false); @@ -113,6 +116,10 @@ export const JobSelectorFlyoutContent: FC = ({ const finalSelection = [...selectedGroupIds, ...standaloneJobs]; const time = applyTimeRangeConfig ? getTimeRangeFromSelection(jobs, finalSelection) : undefined; + if (onTimeRangeConfigChange && initialApplyTimeRangeConfig !== applyTimeRangeConfig) { + onTimeRangeConfigChange(applyTimeRangeConfig); + } + onSelectionConfirmed({ newSelection: finalSelection, jobIds: finalSelection, @@ -126,9 +133,7 @@ export const JobSelectorFlyoutContent: FC = ({ } function toggleTimerangeSwitch() { - if (onTimeRangeConfigChange) { - onTimeRangeConfigChange(!applyTimeRangeConfig); - } + setApplyTimeRangeConfig((prev) => !prev); } function clearSelection() { @@ -242,9 +247,7 @@ export const JobSelectorFlyoutContent: FC = ({ )} - {withTimeRangeSelector && - applyTimeRangeConfig !== undefined && - jobs.length !== 0 ? ( + {withTimeRangeSelector && jobs.length !== 0 ? ( ; */ export function useJobSelectionFlyout() { const { overlays, services } = useMlKibana(); + const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useStorage( + ML_APPLY_TIME_RANGE_CONFIG, + true + ); const flyoutRef = useRef>(); @@ -40,10 +46,12 @@ export function useJobSelectionFlyout() { singleSelection?: boolean; withTimeRangeSelector?: boolean; timeseriesOnly?: boolean; + selectedIds?: string[]; } = { singleSelection: false, withTimeRangeSelector: true, timeseriesOnly: false, + selectedIds: [], } ): Promise => { const { uiSettings } = services; @@ -56,8 +64,10 @@ export function useJobSelectionFlyout() { flyoutRef.current = overlays.openFlyout(