[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
This commit is contained in:
Robert Jaszczurek 2025-03-11 14:50:52 +01:00 committed by GitHub
parent 122c7e12e6
commit cbcb7edb94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 48 additions and 72 deletions

View file

@ -5,33 +5,26 @@
* 2.0.
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import {
EuiButtonEmpty,
EuiFlexItem,
EuiFlexGroup,
EuiFlyout,
EuiHorizontalRule,
} from '@elastic/eui';
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);
}
const handleJobSelectionClick = useCallback(async () => {
try {
const result = await openJobSelectionFlyout({
singleSelection,
withTimeRangeSelector: true,
timeseriesOnly,
selectedIds,
});
function showFlyout() {
setIsFlyoutVisible(true);
}
function handleJobSelectionClick() {
showFlyout();
}
const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback(
({ newSelection, jobIds, time }) => {
setSelectedIds(newSelection);
onSelectionChange?.({ jobIds, time });
closeFlyout();
},
[onSelectionChange]
);
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 (
<EuiFlyout
onClose={closeFlyout}
data-test-subj="mlFlyoutJobSelector"
aria-labelledby="jobSelectorFlyout"
>
<JobSelectorFlyoutContent
dateFormatTz={dateFormatTz}
timeseriesOnly={timeseriesOnly}
singleSelection={singleSelection}
selectedIds={selectedIds}
onSelectionConfirmed={applySelection}
onFlyoutClose={closeFlyout}
applyTimeRangeConfig={applyTimeRangeConfig}
onTimeRangeConfigChange={setApplyTimeRangeConfig}
/>
</EuiFlyout>
);
}
}
return (
<div>
<JobInfoFlyoutsProvider>
{renderJobSelectionBar()}
{renderFlyout()}
<JobInfoFlyoutsManager />
</JobInfoFlyoutsProvider>
</div>

View file

@ -73,7 +73,7 @@ export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
onJobsFetched,
onSelectionConfirmed,
onFlyoutClose,
applyTimeRangeConfig,
applyTimeRangeConfig: initialApplyTimeRangeConfig,
onTimeRangeConfigChange,
withTimeRangeSelector = true,
}) => {
@ -85,6 +85,9 @@ export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
} = 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<JobSelectorFlyoutProps> = ({
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<JobSelectorFlyoutProps> = ({
}
function toggleTimerangeSwitch() {
if (onTimeRangeConfigChange) {
onTimeRangeConfigChange(!applyTimeRangeConfig);
}
setApplyTimeRangeConfig((prev) => !prev);
}
function clearSelection() {
@ -242,9 +247,7 @@ export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
</EuiButtonEmpty>
)}
</EuiFlexItem>
{withTimeRangeSelector &&
applyTimeRangeConfig !== undefined &&
jobs.length !== 0 ? (
{withTimeRangeSelector && jobs.length !== 0 ? (
<EuiFlexItem grow={false}>
<EuiSwitch
label={i18n.translate(

View file

@ -9,6 +9,8 @@ import React, { useCallback, useEffect, useRef } from 'react';
import moment from 'moment';
import type { KibanaReactOverlays } from '@kbn/kibana-react-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage';
import { useMlKibana } from '../kibana';
import {
JobSelectorFlyoutContent,
@ -23,6 +25,10 @@ export type GetJobSelection = ReturnType<typeof useJobSelectionFlyout>;
*/
export function useJobSelectionFlyout() {
const { overlays, services } = useMlKibana();
const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useStorage(
ML_APPLY_TIME_RANGE_CONFIG,
true
);
const flyoutRef = useRef<ReturnType<KibanaReactOverlays['openFlyout']>>();
@ -40,10 +46,12 @@ export function useJobSelectionFlyout() {
singleSelection?: boolean;
withTimeRangeSelector?: boolean;
timeseriesOnly?: boolean;
selectedIds?: string[];
} = {
singleSelection: false,
withTimeRangeSelector: true,
timeseriesOnly: false,
selectedIds: [],
}
): Promise<JobSelectionResult> => {
const { uiSettings } = services;
@ -56,8 +64,10 @@ export function useJobSelectionFlyout() {
flyoutRef.current = overlays.openFlyout(
<KibanaContextProvider services={services}>
<JobSelectorFlyoutContent
selectedIds={[]}
selectedIds={config.selectedIds}
withTimeRangeSelector={config.withTimeRangeSelector}
applyTimeRangeConfig={applyTimeRangeConfig}
onTimeRangeConfigChange={setApplyTimeRangeConfig}
dateFormatTz={dateFormatTz}
singleSelection={!!config.singleSelection}
timeseriesOnly={!!config.timeseriesOnly}
@ -77,6 +87,6 @@ export function useJobSelectionFlyout() {
}
});
},
[overlays, services]
[services, overlays, applyTimeRangeConfig, setApplyTimeRangeConfig]
);
}