Skip to content

Commit 9dffa3d

Browse files
darnautovDmitrii Arnautov
authored andcommitted
[ML] Fix job selection flyout (#79850)
* [ML] fix job selection flyout * [ML] hide time range column * [ML] show callout when no AD jobs presented * [ML] close job selector flyout on navigating away from the dashboard * [ML] add Create job button * [ML] fix mocks * [ML] add unit test for callout
1 parent 2549191 commit 9dffa3d

File tree

7 files changed

+260
-153
lines changed

7 files changed

+260
-153
lines changed

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66

77
import React, { useState, useEffect } from 'react';
88

9-
import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
9+
import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
1111

1212
import { Dictionary } from '../../../../common/types/common';
1313
import { useUrlState } from '../../util/url_state';
1414
// @ts-ignore
1515
import { IdBadges } from './id_badges/index';
16-
import { BADGE_LIMIT, JobSelectorFlyout, JobSelectorFlyoutProps } from './job_selector_flyout';
16+
import {
17+
BADGE_LIMIT,
18+
JobSelectorFlyoutContent,
19+
JobSelectorFlyoutProps,
20+
} from './job_selector_flyout';
1721
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
1822

1923
interface GroupObj {
@@ -163,16 +167,18 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J
163167
function renderFlyout() {
164168
if (isFlyoutVisible) {
165169
return (
166-
<JobSelectorFlyout
167-
dateFormatTz={dateFormatTz}
168-
timeseriesOnly={timeseriesOnly}
169-
singleSelection={singleSelection}
170-
selectedIds={selectedIds}
171-
onSelectionConfirmed={applySelection}
172-
onJobsFetched={setMaps}
173-
onFlyoutClose={closeFlyout}
174-
maps={maps}
175-
/>
170+
<EuiFlyout onClose={closeFlyout}>
171+
<JobSelectorFlyoutContent
172+
dateFormatTz={dateFormatTz}
173+
timeseriesOnly={timeseriesOnly}
174+
singleSelection={singleSelection}
175+
selectedIds={selectedIds}
176+
onSelectionConfirmed={applySelection}
177+
onJobsFetched={setMaps}
178+
onFlyoutClose={closeFlyout}
179+
maps={maps}
180+
/>
181+
</EuiFlyout>
176182
);
177183
}
178184
}

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

Lines changed: 130 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
EuiButtonEmpty,
1212
EuiFlexItem,
1313
EuiFlexGroup,
14-
EuiFlyout,
1514
EuiFlyoutBody,
1615
EuiFlyoutFooter,
1716
EuiFlyoutHeader,
1817
EuiSwitch,
1918
EuiTitle,
19+
EuiResizeObserver,
20+
EuiProgress,
2021
} from '@elastic/eui';
2122
import { NewSelectionIdBadges } from './new_selection_id_badges';
2223
// @ts-ignore
@@ -39,7 +40,6 @@ export interface JobSelectorFlyoutProps {
3940
newSelection?: string[];
4041
onFlyoutClose: () => void;
4142
onJobsFetched?: (maps: JobSelectionMaps) => void;
42-
onSelectionChange?: (newSelection: string[]) => void;
4343
onSelectionConfirmed: (payload: {
4444
newSelection: string[];
4545
jobIds: string[];
@@ -52,13 +52,12 @@ export interface JobSelectorFlyoutProps {
5252
withTimeRangeSelector?: boolean;
5353
}
5454

55-
export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
55+
export const JobSelectorFlyoutContent: FC<JobSelectorFlyoutProps> = ({
5656
dateFormatTz,
5757
selectedIds = [],
5858
singleSelection,
5959
timeseriesOnly,
6060
onJobsFetched,
61-
onSelectionChange,
6261
onSelectionConfirmed,
6362
onFlyoutClose,
6463
maps,
@@ -73,14 +72,15 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
7372

7473
const [newSelection, setNewSelection] = useState(selectedIds);
7574

75+
const [isLoading, setIsLoading] = useState(true);
7676
const [showAllBadges, setShowAllBadges] = useState(false);
7777
const [applyTimeRange, setApplyTimeRange] = useState(true);
7878
const [jobs, setJobs] = useState<MlJobWithTimeRange[]>([]);
7979
const [groups, setGroups] = useState<any[]>([]);
8080
const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH);
8181
const [jobGroupsMaps, setJobGroupsMaps] = useState(maps);
8282

83-
const flyoutEl = useRef<{ flyout: HTMLElement }>(null);
83+
const flyoutEl = useRef<HTMLElement | null>(null);
8484

8585
function applySelection() {
8686
// allNewSelection will be a list of all job ids (including those from groups) selected from the table
@@ -131,19 +131,19 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
131131
// Wrap handleResize in useCallback as it is a dependency for useEffect on line 131 below.
132132
// Not wrapping it would cause this dependency to change on every render
133133
const handleResize = useCallback(() => {
134-
if (jobs.length > 0 && flyoutEl && flyoutEl.current && flyoutEl.current.flyout) {
135-
// get all cols in flyout table
136-
const tableHeaderCols: NodeListOf<HTMLElement> = flyoutEl.current.flyout.querySelectorAll(
137-
'table thead th'
138-
);
139-
// get the width of the last col
140-
const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16;
141-
const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth);
142-
setJobs(normalizedJobs);
143-
const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs);
144-
setGroups(updatedGroups);
145-
setGanttBarWidth(derivedWidth);
146-
}
134+
if (jobs.length === 0 || !flyoutEl.current) return;
135+
136+
// get all cols in flyout table
137+
const tableHeaderCols: NodeListOf<HTMLElement> = flyoutEl.current.querySelectorAll(
138+
'table thead th'
139+
);
140+
// get the width of the last col
141+
const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16;
142+
const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth);
143+
setJobs(normalizedJobs);
144+
const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs);
145+
setGroups(updatedGroups);
146+
setGanttBarWidth(derivedWidth);
147147
}, [dateFormatTz, jobs]);
148148

149149
// Fetch jobs list on flyout open
@@ -172,119 +172,124 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
172172
}),
173173
});
174174
}
175+
setIsLoading(false);
175176
}
176177

177-
useEffect(() => {
178-
// Ensure ganttBar width gets calculated on resize
179-
window.addEventListener('resize', handleResize);
180-
181-
return () => {
182-
window.removeEventListener('resize', handleResize);
183-
};
184-
}, [handleResize]);
185-
186-
useEffect(() => {
187-
handleResize();
188-
}, [handleResize, jobs]);
189-
190178
return (
191-
<EuiFlyout
192-
// @ts-ignore
193-
ref={flyoutEl}
194-
onClose={onFlyoutClose}
195-
aria-labelledby="jobSelectorFlyout"
196-
data-test-subj="mlFlyoutJobSelector"
197-
>
198-
<EuiFlyoutHeader hasBorder>
199-
<EuiTitle size="m">
200-
<h2 id="flyoutTitle">
201-
{i18n.translate('xpack.ml.jobSelector.flyoutTitle', {
202-
defaultMessage: 'Job selection',
203-
})}
204-
</h2>
205-
</EuiTitle>
206-
</EuiFlyoutHeader>
207-
<EuiFlyoutBody className="mlJobSelectorFlyoutBody">
208-
<EuiFlexGroup direction="column" responsive={false}>
209-
<EuiFlexItem grow={false}>
210-
<EuiFlexGroup wrap responsive={false} gutterSize="xs" alignItems="center">
211-
<NewSelectionIdBadges
212-
limit={BADGE_LIMIT}
213-
maps={jobGroupsMaps}
214-
newSelection={newSelection}
215-
onDeleteClick={removeId}
216-
onLinkClick={() => setShowAllBadges(!showAllBadges)}
217-
showAllBadges={showAllBadges}
218-
/>
219-
</EuiFlexGroup>
220-
</EuiFlexItem>
221-
<EuiFlexItem grow={false}>
222-
<EuiFlexGroup direction="row" justifyContent="spaceBetween" responsive={false}>
179+
<EuiResizeObserver onResize={handleResize}>
180+
{(resizeRef) => (
181+
<EuiFlexGroup
182+
direction="column"
183+
gutterSize="none"
184+
ref={(e) => {
185+
flyoutEl.current = e;
186+
resizeRef(e);
187+
}}
188+
aria-labelledby="jobSelectorFlyout"
189+
data-test-subj="mlFlyoutJobSelector"
190+
>
191+
<EuiFlyoutHeader hasBorder>
192+
<EuiTitle size="m">
193+
<h2 id="flyoutTitle">
194+
{i18n.translate('xpack.ml.jobSelector.flyoutTitle', {
195+
defaultMessage: 'Job selection',
196+
})}
197+
</h2>
198+
</EuiTitle>
199+
</EuiFlyoutHeader>
200+
<EuiFlyoutBody className="mlJobSelectorFlyoutBody">
201+
{isLoading ? (
202+
<EuiProgress size="xs" color="accent" />
203+
) : (
204+
<>
205+
<EuiFlexGroup direction="column" responsive={false}>
206+
<EuiFlexItem grow={false}>
207+
<EuiFlexGroup wrap responsive={false} gutterSize="xs" alignItems="center">
208+
<NewSelectionIdBadges
209+
limit={BADGE_LIMIT}
210+
maps={jobGroupsMaps}
211+
newSelection={newSelection}
212+
onDeleteClick={removeId}
213+
onLinkClick={() => setShowAllBadges(!showAllBadges)}
214+
showAllBadges={showAllBadges}
215+
/>
216+
</EuiFlexGroup>
217+
</EuiFlexItem>
218+
<EuiFlexItem grow={false}>
219+
<EuiFlexGroup direction="row" justifyContent="spaceBetween" responsive={false}>
220+
<EuiFlexItem grow={false}>
221+
{!singleSelection && newSelection.length > 0 && (
222+
<EuiButtonEmpty
223+
onClick={clearSelection}
224+
size="xs"
225+
data-test-subj="mlFlyoutJobSelectorButtonClearSelection"
226+
>
227+
{i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', {
228+
defaultMessage: 'Clear all',
229+
})}
230+
</EuiButtonEmpty>
231+
)}
232+
</EuiFlexItem>
233+
{withTimeRangeSelector && (
234+
<EuiFlexItem grow={false}>
235+
<EuiSwitch
236+
label={i18n.translate(
237+
'xpack.ml.jobSelector.applyTimerangeSwitchLabel',
238+
{
239+
defaultMessage: 'Apply time range',
240+
}
241+
)}
242+
checked={applyTimeRange}
243+
onChange={toggleTimerangeSwitch}
244+
data-test-subj="mlFlyoutJobSelectorSwitchApplyTimeRange"
245+
/>
246+
</EuiFlexItem>
247+
)}
248+
</EuiFlexGroup>
249+
</EuiFlexItem>
250+
</EuiFlexGroup>
251+
<JobSelectorTable
252+
jobs={jobs}
253+
ganttBarWidth={ganttBarWidth}
254+
groupsList={groups}
255+
onSelection={handleNewSelection}
256+
selectedIds={newSelection}
257+
singleSelection={singleSelection}
258+
timeseriesOnly={timeseriesOnly}
259+
withTimeRangeSelector={withTimeRangeSelector}
260+
/>
261+
</>
262+
)}
263+
</EuiFlyoutBody>
264+
<EuiFlyoutFooter>
265+
<EuiFlexGroup>
223266
<EuiFlexItem grow={false}>
224-
{!singleSelection && newSelection.length > 0 && (
225-
<EuiButtonEmpty
226-
onClick={clearSelection}
227-
size="xs"
228-
data-test-subj="mlFlyoutJobSelectorButtonClearSelection"
229-
>
230-
{i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', {
231-
defaultMessage: 'Clear all',
232-
})}
233-
</EuiButtonEmpty>
234-
)}
267+
<EuiButton
268+
onClick={applySelection}
269+
fill
270+
isDisabled={newSelection.length === 0}
271+
data-test-subj="mlFlyoutJobSelectorButtonApply"
272+
>
273+
{i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', {
274+
defaultMessage: 'Apply',
275+
})}
276+
</EuiButton>
277+
</EuiFlexItem>
278+
<EuiFlexItem grow={false}>
279+
<EuiButtonEmpty
280+
iconType="cross"
281+
onClick={onFlyoutClose}
282+
data-test-subj="mlFlyoutJobSelectorButtonClose"
283+
>
284+
{i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', {
285+
defaultMessage: 'Close',
286+
})}
287+
</EuiButtonEmpty>
235288
</EuiFlexItem>
236-
{withTimeRangeSelector && (
237-
<EuiFlexItem grow={false}>
238-
<EuiSwitch
239-
label={i18n.translate('xpack.ml.jobSelector.applyTimerangeSwitchLabel', {
240-
defaultMessage: 'Apply time range',
241-
})}
242-
checked={applyTimeRange}
243-
onChange={toggleTimerangeSwitch}
244-
data-test-subj="mlFlyoutJobSelectorSwitchApplyTimeRange"
245-
/>
246-
</EuiFlexItem>
247-
)}
248289
</EuiFlexGroup>
249-
</EuiFlexItem>
250-
</EuiFlexGroup>
251-
<JobSelectorTable
252-
jobs={jobs}
253-
ganttBarWidth={ganttBarWidth}
254-
groupsList={groups}
255-
onSelection={handleNewSelection}
256-
selectedIds={newSelection}
257-
singleSelection={singleSelection}
258-
timeseriesOnly={timeseriesOnly}
259-
/>
260-
</EuiFlyoutBody>
261-
<EuiFlyoutFooter>
262-
<EuiFlexGroup>
263-
<EuiFlexItem grow={false}>
264-
<EuiButton
265-
onClick={applySelection}
266-
fill
267-
isDisabled={newSelection.length === 0}
268-
data-test-subj="mlFlyoutJobSelectorButtonApply"
269-
>
270-
{i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', {
271-
defaultMessage: 'Apply',
272-
})}
273-
</EuiButton>
274-
</EuiFlexItem>
275-
<EuiFlexItem grow={false}>
276-
<EuiButtonEmpty
277-
iconType="cross"
278-
onClick={onFlyoutClose}
279-
data-test-subj="mlFlyoutJobSelectorButtonClose"
280-
>
281-
{i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', {
282-
defaultMessage: 'Close',
283-
})}
284-
</EuiButtonEmpty>
285-
</EuiFlexItem>
290+
</EuiFlyoutFooter>
286291
</EuiFlexGroup>
287-
</EuiFlyoutFooter>
288-
</EuiFlyout>
292+
)}
293+
</EuiResizeObserver>
289294
);
290295
};

0 commit comments

Comments
 (0)