Skip to content

Commit f453467

Browse files
authored
[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
1 parent 1290ef4 commit f453467

File tree

12 files changed

+123
-109
lines changed

12 files changed

+123
-109
lines changed

x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { useState, useEffect } from 'react';
7+
import React, { useState, useEffect, useCallback } from 'react';
88

99
import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
@@ -109,24 +109,22 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J
109109
showFlyout();
110110
}
111111

112-
const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({
113-
newSelection,
114-
jobIds,
115-
groups: newGroups,
116-
time,
117-
}) => {
118-
setSelectedIds(newSelection);
119-
120-
setGlobalState({
121-
ml: {
122-
jobIds,
123-
groups: newGroups,
124-
},
125-
...(time !== undefined ? { time } : {}),
126-
});
127-
128-
closeFlyout();
129-
};
112+
const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback(
113+
({ newSelection, jobIds, groups: newGroups, time }) => {
114+
setSelectedIds(newSelection);
115+
116+
setGlobalState({
117+
ml: {
118+
jobIds,
119+
groups: newGroups,
120+
},
121+
...(time !== undefined ? { time } : {}),
122+
});
123+
124+
closeFlyout();
125+
},
126+
[setGlobalState, setSelectedIds]
127+
);
130128

131129
function renderJobSelectionBar() {
132130
return (

x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
8282

8383
const flyoutEl = useRef<HTMLElement | null>(null);
8484

85-
function applySelection() {
85+
const applySelection = useCallback(() => {
8686
// allNewSelection will be a list of all job ids (including those from groups) selected from the table
8787
const allNewSelection: string[] = [];
8888
const groupSelection: Array<{ groupId: string; jobIds: string[] }> = [];
@@ -110,7 +110,7 @@ export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
110110
groups: groupSelection,
111111
time,
112112
});
113-
}
113+
}, [onSelectionConfirmed, newSelection, jobGroupsMaps, applyTimeRange]);
114114

115115
function removeId(id: string) {
116116
setNewSelection(newSelection.filter((item) => item !== id));

x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[]) => {
8888
...(time !== undefined ? { time } : {}),
8989
});
9090
}
91-
}, [jobs, validIds]);
91+
}, [jobs, validIds, setGlobalState, globalState?.ml]);
9292

9393
return jobSelection;
9494
};

x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const DatePickerWrapper: FC = () => {
5454
useEffect(() => {
5555
setGlobalState({ refreshInterval });
5656
timefilter.setRefreshInterval(refreshInterval);
57-
}, [refreshInterval?.pause, refreshInterval?.value]);
57+
}, [refreshInterval?.pause, refreshInterval?.value, setGlobalState]);
5858

5959
const [time, setTime] = useState(timefilter.getTime());
6060
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges());

x-pack/plugins/ml/public/application/explorer/explorer_utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export function getSelectionTimeRange(selectedCells, interval, bounds) {
198198
latestMs = bounds.max.valueOf();
199199
if (selectedCells.times[1] !== undefined) {
200200
// Subtract 1 ms so search does not include start of next bucket.
201-
latestMs = (selectedCells.times[1] + interval) * 1000 - 1;
201+
latestMs = selectedCells.times[1] * 1000 - 1;
202202
}
203203
}
204204

x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const useSelectedCells = (
6060
setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
6161
}
6262
},
63-
[appState?.mlExplorerSwimlane, selectedCells]
63+
[appState?.mlExplorerSwimlane, selectedCells, setAppState]
6464
);
6565

6666
return [selectedCells, setSelectedCells];

x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx

Lines changed: 74 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils';
4141

4242
import './_explorer.scss';
4343
import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control';
44+
import { useUiSettings } from '../contexts/kibana';
4445

4546
/**
4647
* Ignore insignificant resize, e.g. browser scrollbar appearance.
@@ -159,6 +160,8 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
159160
}) => {
160161
const [chartWidth, setChartWidth] = useState<number>(0);
161162

163+
const isDarkTheme = !!useUiSettings().get('theme:darkMode');
164+
162165
// Holds the container height for previously fetched data
163166
const containerHeightRef = useRef<number>();
164167

@@ -235,67 +238,76 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
235238
return { x: selection.times.map((v) => v * 1000), y: selection.lanes };
236239
}, [selection, swimlaneData, swimlaneType]);
237240

238-
const swimLaneConfig: HeatmapSpec['config'] = useMemo(
239-
() =>
240-
showSwimlane
241-
? {
242-
onBrushEnd: (e: HeatmapBrushEvent) => {
243-
onCellsSelection({
244-
lanes: e.y as string[],
245-
times: e.x.map((v) => (v as number) / 1000),
246-
type: swimlaneType,
247-
viewByFieldName: swimlaneData.fieldName,
248-
});
249-
},
250-
grid: {
251-
cellHeight: {
252-
min: CELL_HEIGHT,
253-
max: CELL_HEIGHT,
254-
},
255-
stroke: {
256-
width: 1,
257-
color: '#D3DAE6',
258-
},
259-
},
260-
cell: {
261-
maxWidth: 'fill',
262-
maxHeight: 'fill',
263-
label: {
264-
visible: false,
265-
},
266-
border: {
267-
stroke: '#D3DAE6',
268-
strokeWidth: 0,
269-
},
270-
},
271-
yAxisLabel: {
272-
visible: true,
273-
width: 170,
274-
// eui color subdued
275-
fill: `#6a717d`,
276-
padding: 8,
277-
formatter: (laneLabel: string) => {
278-
return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel;
279-
},
280-
},
281-
xAxisLabel: {
282-
visible: showTimeline,
283-
// eui color subdued
284-
fill: `#98A2B3`,
285-
formatter: (v: number) => {
286-
timeBuckets.setInterval(`${swimlaneData.interval}s`);
287-
const a = timeBuckets.getScaledDateFormat();
288-
return moment(v).format(a);
289-
},
290-
},
291-
brushMask: {
292-
fill: 'rgb(247 247 247 / 50%)',
293-
},
294-
maxLegendHeight: LEGEND_HEIGHT,
295-
}
296-
: {},
297-
[showSwimlane, swimlaneType, swimlaneData?.fieldName]
298-
);
241+
const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => {
242+
if (!showSwimlane) return {};
243+
244+
return {
245+
onBrushEnd: (e: HeatmapBrushEvent) => {
246+
onCellsSelection({
247+
lanes: e.y as string[],
248+
times: e.x.map((v) => (v as number) / 1000),
249+
type: swimlaneType,
250+
viewByFieldName: swimlaneData.fieldName,
251+
});
252+
},
253+
grid: {
254+
cellHeight: {
255+
min: CELL_HEIGHT,
256+
max: CELL_HEIGHT,
257+
},
258+
stroke: {
259+
width: 1,
260+
color: '#D3DAE6',
261+
},
262+
},
263+
cell: {
264+
maxWidth: 'fill',
265+
maxHeight: 'fill',
266+
label: {
267+
visible: false,
268+
},
269+
border: {
270+
stroke: '#D3DAE6',
271+
strokeWidth: 0,
272+
},
273+
},
274+
yAxisLabel: {
275+
visible: true,
276+
width: 170,
277+
// eui color subdued
278+
fill: `#6a717d`,
279+
padding: 8,
280+
formatter: (laneLabel: string) => {
281+
return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel;
282+
},
283+
},
284+
xAxisLabel: {
285+
visible: showTimeline,
286+
// eui color subdued
287+
fill: `#98A2B3`,
288+
formatter: (v: number) => {
289+
timeBuckets.setInterval(`${swimlaneData.interval}s`);
290+
const scaledDateFormat = timeBuckets.getScaledDateFormat();
291+
return moment(v).format(scaledDateFormat);
292+
},
293+
},
294+
brushMask: {
295+
fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)',
296+
},
297+
brushArea: {
298+
stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)',
299+
},
300+
maxLegendHeight: LEGEND_HEIGHT,
301+
timeZone: 'UTC',
302+
};
303+
}, [
304+
showSwimlane,
305+
swimlaneType,
306+
swimlaneData?.fieldName,
307+
isDarkTheme,
308+
timeBuckets,
309+
onCellsSelection,
310+
]);
299311

300312
// @ts-ignore
301313
const onElementClick: ElementClickListener = useCallback(
@@ -310,7 +322,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
310322
};
311323
onCellsSelection(payload);
312324
},
313-
[swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval]
325+
[swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval, onCellsSelection]
314326
);
315327

316328
const tooltipOptions: TooltipSettings = useMemo(

x-pack/plugins/ml/public/application/routing/routes/explorer.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
8282
const { jobIds } = useJobSelection(jobsWithTimeRange);
8383

8484
const refresh = useRefresh();
85+
8586
useEffect(() => {
86-
if (refresh !== undefined) {
87+
if (refresh !== undefined && lastRefresh !== refresh.lastRefresh) {
8788
setLastRefresh(refresh?.lastRefresh);
8889

8990
if (refresh.timeRange !== undefined) {
@@ -94,7 +95,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
9495
});
9596
}
9697
}
97-
}, [refresh?.lastRefresh]);
98+
}, [refresh?.lastRefresh, lastRefresh, setLastRefresh, setGlobalState]);
9899

99100
// We cannot simply infer bounds from the globalState's `time` attribute
100101
// with `moment` since it can contain custom strings such as `now-15m`.
@@ -194,6 +195,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
194195
const [tableSeverity] = useTableSeverity();
195196

196197
const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState);
198+
197199
useEffect(() => {
198200
explorerService.setSelectedCells(selectedCells);
199201
}, [JSON.stringify(selectedCells)]);
@@ -220,9 +222,9 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
220222
if (explorerState && explorerState.swimlaneContainerWidth > 0) {
221223
loadExplorerData({
222224
...loadExplorerDataConfig,
223-
swimlaneLimit:
224-
isViewBySwimLaneData(explorerState?.viewBySwimlaneData) &&
225-
explorerState?.viewBySwimlaneData.cardinality,
225+
swimlaneLimit: isViewBySwimLaneData(explorerState?.viewBySwimlaneData)
226+
? explorerState?.viewBySwimlaneData.cardinality
227+
: undefined,
226228
});
227229
}
228230
}, [JSON.stringify(loadExplorerDataConfig)]);

x-pack/plugins/ml/public/application/util/url_state.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ export const useUrlState = (accessor: Accessor) => {
140140
if (typeof fullUrlState === 'object') {
141141
return fullUrlState[accessor];
142142
}
143-
return undefined;
144143
}, [searchString]);
145144

146145
const setUrlState = useCallback(
147-
(attribute: string | Dictionary<any>, value?: any) =>
148-
setUrlStateContext(accessor, attribute, value),
146+
(attribute: string | Dictionary<any>, value?: any) => {
147+
setUrlStateContext(accessor, attribute, value);
148+
},
149149
[accessor, setUrlStateContext]
150150
);
151151
return [urlState, setUrlState];

x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ReactDOM from 'react-dom';
99
import { CoreStart } from 'kibana/public';
1010
import { i18n } from '@kbn/i18n';
1111
import { Subject } from 'rxjs';
12+
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
1213
import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public';
1314
import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy';
1415
import type { JobId } from '../../../common/types/anomaly_detection_jobs';
@@ -59,17 +60,19 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
5960

6061
ReactDOM.render(
6162
<I18nContext>
62-
<Suspense fallback={null}>
63-
<EmbeddableSwimLaneContainer
64-
id={this.input.id}
65-
embeddableContext={this}
66-
embeddableInput={this.getInput$()}
67-
services={this.services}
68-
refresh={this.reload$.asObservable()}
69-
onInputChange={this.updateInput.bind(this)}
70-
onOutputChange={this.updateOutput.bind(this)}
71-
/>
72-
</Suspense>
63+
<KibanaContextProvider services={{ ...this.services[0] }}>
64+
<Suspense fallback={null}>
65+
<EmbeddableSwimLaneContainer
66+
id={this.input.id}
67+
embeddableContext={this}
68+
embeddableInput={this.getInput$()}
69+
services={this.services}
70+
refresh={this.reload$.asObservable()}
71+
onInputChange={this.updateInput.bind(this)}
72+
onOutputChange={this.updateOutput.bind(this)}
73+
/>
74+
</Suspense>
75+
</KibanaContextProvider>
7376
</I18nContext>,
7477
node
7578
);

0 commit comments

Comments
 (0)