From adc9ebe0efbe14838932a715f4d8d5dfdad89b3d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 13 Oct 2020 16:54:15 +0200 Subject: [PATCH] [ML] Fixes for anomaly swim lane (#80299) * [ML] add swim lane styles for dark theme * [ML] fix global time range update on sell selection * [ML] fix getSelectionTimeRange * [ML] fix range selection for embeddable * [ML] fix job selection * [ML] fix swim lane limit --- .../components/job_selector/job_selector.tsx | 36 +++-- .../job_selector/job_selector_flyout.tsx | 4 +- .../job_selector/use_job_selection.ts | 2 +- .../date_picker_wrapper.tsx | 2 +- .../application/explorer/explorer_utils.js | 2 +- .../explorer/hooks/use_selected_cells.ts | 2 +- .../explorer/swimlane_container.tsx | 136 ++++++++++-------- .../application/routing/routes/explorer.tsx | 12 +- .../ml/public/application/util/url_state.tsx | 6 +- .../anomaly_swimlane_embeddable.tsx | 25 ++-- .../embeddable_swim_lane_container.tsx | 2 +- .../ui_actions/apply_time_range_action.tsx | 3 +- 12 files changed, 123 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index a00284860d668..d25d2c3a858ed 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -109,24 +109,22 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J showFlyout(); } - const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({ - newSelection, - jobIds, - groups: newGroups, - time, - }) => { - setSelectedIds(newSelection); - - setGlobalState({ - ml: { - jobIds, - groups: newGroups, - }, - ...(time !== undefined ? { time } : {}), - }); - - closeFlyout(); - }; + const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback( + ({ newSelection, jobIds, groups: newGroups, time }) => { + setSelectedIds(newSelection); + + setGlobalState({ + ml: { + jobIds, + groups: newGroups, + }, + ...(time !== undefined ? { time } : {}), + }); + + closeFlyout(); + }, + [setGlobalState, setSelectedIds] + ); function renderJobSelectionBar() { return ( diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx index 1e8ac4c15fd15..a90eb8cfde532 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -82,7 +82,7 @@ export const JobSelectorFlyoutContent: FC = ({ const flyoutEl = useRef(null); - function applySelection() { + const applySelection = useCallback(() => { // allNewSelection will be a list of all job ids (including those from groups) selected from the table const allNewSelection: string[] = []; const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; @@ -110,7 +110,7 @@ export const JobSelectorFlyoutContent: FC = ({ groups: groupSelection, time, }); - } + }, [onSelectionConfirmed, newSelection, jobGroupsMaps, applyTimeRange]); function removeId(id: string) { setNewSelection(newSelection.filter((item) => item !== id)); diff --git a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 0717348d1db22..04fa3e9201c6c 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -88,7 +88,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[]) => { ...(time !== undefined ? { time } : {}), }); } - }, [jobs, validIds]); + }, [jobs, validIds, setGlobalState, globalState?.ml]); return jobSelection; }; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index beafae1ecd2f6..409bd11e0bde3 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -54,7 +54,7 @@ export const DatePickerWrapper: FC = () => { useEffect(() => { setGlobalState({ refreshInterval }); timefilter.setRefreshInterval(refreshInterval); - }, [refreshInterval?.pause, refreshInterval?.value]); + }, [refreshInterval?.pause, refreshInterval?.value, setGlobalState]); const [time, setTime] = useState(timefilter.getTime()); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges()); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index c309e1f4ef8e8..c3bdacde5abd8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -198,7 +198,7 @@ export function getSelectionTimeRange(selectedCells, interval, bounds) { latestMs = bounds.max.valueOf(); if (selectedCells.times[1] !== undefined) { // Subtract 1 ms so search does not include start of next bucket. - latestMs = (selectedCells.times[1] + interval) * 1000 - 1; + latestMs = selectedCells.times[1] * 1000 - 1; } } diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index f356d79c0a8e1..c7cda2372bceb 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -60,7 +60,7 @@ export const useSelectedCells = ( setAppState('mlExplorerSwimlane', mlExplorerSwimlane); } }, - [appState?.mlExplorerSwimlane, selectedCells] + [appState?.mlExplorerSwimlane, selectedCells, setAppState] ); return [selectedCells, setSelectedCells]; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 0a2791edb9c50..9d6d6c14ed659 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -41,6 +41,7 @@ import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; +import { useUiSettings } from '../contexts/kibana'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -159,6 +160,8 @@ export const SwimlaneContainer: FC = ({ }) => { const [chartWidth, setChartWidth] = useState(0); + const isDarkTheme = !!useUiSettings().get('theme:darkMode'); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); @@ -235,67 +238,76 @@ export const SwimlaneContainer: FC = ({ return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; }, [selection, swimlaneData, swimlaneType]); - const swimLaneConfig: HeatmapSpec['config'] = useMemo( - () => - showSwimlane - ? { - onBrushEnd: (e: HeatmapBrushEvent) => { - onCellsSelection({ - lanes: e.y as string[], - times: e.x.map((v) => (v as number) / 1000), - type: swimlaneType, - viewByFieldName: swimlaneData.fieldName, - }); - }, - grid: { - cellHeight: { - min: CELL_HEIGHT, - max: CELL_HEIGHT, - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - formatter: (laneLabel: string) => { - return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; - }, - }, - xAxisLabel: { - visible: showTimeline, - // eui color subdued - fill: `#98A2B3`, - formatter: (v: number) => { - timeBuckets.setInterval(`${swimlaneData.interval}s`); - const a = timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - brushMask: { - fill: 'rgb(247 247 247 / 50%)', - }, - maxLegendHeight: LEGEND_HEIGHT, - } - : {}, - [showSwimlane, swimlaneType, swimlaneData?.fieldName] - ); + const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => { + if (!showSwimlane) return {}; + + return { + onBrushEnd: (e: HeatmapBrushEvent) => { + onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT, + max: CELL_HEIGHT, + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + formatter: (laneLabel: string) => { + return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; + }, + }, + xAxisLabel: { + visible: showTimeline, + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + timeBuckets.setInterval(`${swimlaneData.interval}s`); + const scaledDateFormat = timeBuckets.getScaledDateFormat(); + return moment(v).format(scaledDateFormat); + }, + }, + brushMask: { + fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)', + }, + brushArea: { + stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)', + }, + maxLegendHeight: LEGEND_HEIGHT, + timeZone: 'UTC', + }; + }, [ + showSwimlane, + swimlaneType, + swimlaneData?.fieldName, + isDarkTheme, + timeBuckets, + onCellsSelection, + ]); // @ts-ignore const onElementClick: ElementClickListener = useCallback( @@ -310,7 +322,7 @@ export const SwimlaneContainer: FC = ({ }; onCellsSelection(payload); }, - [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] + [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval, onCellsSelection] ); const tooltipOptions: TooltipSettings = useMemo( diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 00d64a2f1bd1d..cb6944e0ecf05 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -82,8 +82,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const { jobIds } = useJobSelection(jobsWithTimeRange); const refresh = useRefresh(); + useEffect(() => { - if (refresh !== undefined) { + if (refresh !== undefined && lastRefresh !== refresh.lastRefresh) { setLastRefresh(refresh?.lastRefresh); if (refresh.timeRange !== undefined) { @@ -94,7 +95,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim }); } } - }, [refresh?.lastRefresh]); + }, [refresh?.lastRefresh, lastRefresh, setLastRefresh, setGlobalState]); // We cannot simply infer bounds from the globalState's `time` attribute // with `moment` since it can contain custom strings such as `now-15m`. @@ -194,6 +195,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableSeverity] = useTableSeverity(); const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState); + useEffect(() => { explorerService.setSelectedCells(selectedCells); }, [JSON.stringify(selectedCells)]); @@ -220,9 +222,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (explorerState && explorerState.swimlaneContainerWidth > 0) { loadExplorerData({ ...loadExplorerDataConfig, - swimlaneLimit: - isViewBySwimLaneData(explorerState?.viewBySwimlaneData) && - explorerState?.viewBySwimlaneData.cardinality, + swimlaneLimit: isViewBySwimLaneData(explorerState?.viewBySwimlaneData) + ? explorerState?.viewBySwimlaneData.cardinality + : undefined, }); } }, [JSON.stringify(loadExplorerDataConfig)]); diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index c288a00bb06da..a3c70e1130904 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -140,12 +140,12 @@ export const useUrlState = (accessor: Accessor) => { if (typeof fullUrlState === 'object') { return fullUrlState[accessor]; } - return undefined; }, [searchString]); const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any) => - setUrlStateContext(accessor, attribute, value), + (attribute: string | Dictionary, value?: any) => { + setUrlStateContext(accessor, attribute, value); + }, [accessor, setUrlStateContext] ); return [urlState, setUrlState]; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 6e67ff1aef03d..4730371c611c1 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -59,17 +60,19 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ReactDOM.render( - - - + + + + + , node ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 17ae97e3c07bb..5efe70ba552f5 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -89,7 +89,7 @@ export const EmbeddableSwimLaneContainer: FC = ( }); } }, - [swimlaneData, perPage, fromPage] + [swimlaneData, perPage, fromPage, setSelectedCells] ); if (error) { diff --git a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx index 325e903de0e2d..79e6ff53bff43 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx @@ -39,8 +39,7 @@ export function createApplyTimeRangeSelectionAction( let [from, to] = data.times; from = from * 1000; - // extend bounds with the interval - to = to * 1000 + interval * 1000; + to = to * 1000; timefilter.setTime({ from: moment(from),