Skip to content

Commit 70cea48

Browse files
[ML] DF Analytics jobs list: persist pagination through refresh interval (#75996)
* wip: switch analyticsList inMemoryTable to basic and implement search bar * move basicTable settings to custom hook and update types * update types * add types for empty prompt * ensure sorting works * add refresh to analytics management list * ensure table still updates editing job
1 parent 4762cf5 commit 70cea48

File tree

8 files changed

+446
-138
lines changed

8 files changed

+446
-138
lines changed

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx

Lines changed: 110 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
*/
66

77
import React, { FC, useCallback, useState, useEffect } from 'react';
8-
98
import { i18n } from '@kbn/i18n';
10-
119
import {
12-
Direction,
13-
EuiButton,
1410
EuiCallOut,
15-
EuiEmptyPrompt,
1611
EuiFlexGroup,
1712
EuiFlexItem,
18-
EuiInMemoryTable,
13+
EuiBasicTable,
14+
EuiSearchBar,
1915
EuiSearchBarProps,
2016
EuiSpacer,
2117
} from '@elastic/eui';
@@ -43,6 +39,39 @@ import {
4339
getGroupQueryText,
4440
} from '../../../../../jobs/jobs_list/components/utils';
4541
import { SourceSelection } from '../source_selection';
42+
import { filterAnalytics, AnalyticsSearchBar } from '../analytics_search_bar';
43+
import { AnalyticsEmptyPrompt } from './empty_prompt';
44+
import { useTableSettings } from './use_table_settings';
45+
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
46+
47+
const filters: EuiSearchBarProps['filters'] = [
48+
{
49+
type: 'field_value_selection',
50+
field: 'job_type',
51+
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
52+
defaultMessage: 'Type',
53+
}),
54+
multiSelect: 'or',
55+
options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({
56+
value: val,
57+
name: val,
58+
view: getJobTypeBadge(val),
59+
})),
60+
},
61+
{
62+
type: 'field_value_selection',
63+
field: 'state',
64+
name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', {
65+
defaultMessage: 'Status',
66+
}),
67+
multiSelect: 'or',
68+
options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({
69+
value: val,
70+
name: val,
71+
view: getTaskStateBadge(val),
72+
})),
73+
},
74+
];
4675

4776
function getItemIdToExpandedRowMap(
4877
itemIds: DataFrameAnalyticsId[],
@@ -70,23 +99,23 @@ export const DataFrameAnalyticsList: FC<Props> = ({
7099
const [isInitialized, setIsInitialized] = useState(false);
71100
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
72101
const [isLoading, setIsLoading] = useState(false);
73-
102+
const [filteredAnalytics, setFilteredAnalytics] = useState<{
103+
active: boolean;
104+
items: DataFrameAnalyticsListRow[];
105+
}>({
106+
active: false,
107+
items: [],
108+
});
74109
const [searchQueryText, setSearchQueryText] = useState('');
75-
76110
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
77111
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
78112
undefined
79113
);
80114
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
81-
82115
const [errorMessage, setErrorMessage] = useState<any>(undefined);
83-
const [searchError, setSearchError] = useState<any>(undefined);
84-
85-
const [pageIndex, setPageIndex] = useState(0);
86-
const [pageSize, setPageSize] = useState(10);
87-
88-
const [sortField, setSortField] = useState<string>(DataFrameAnalyticsListColumn.id);
89-
const [sortDirection, setSortDirection] = useState<Direction>('asc');
116+
// Query text/job_id based on url but only after getAnalytics is done first
117+
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
118+
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
90119

91120
const disabled =
92121
!checkPermission('canCreateDataFrameAnalytics') ||
@@ -100,9 +129,29 @@ export const DataFrameAnalyticsList: FC<Props> = ({
100129
blockRefresh
101130
);
102131

103-
// Query text/job_id based on url but only after getAnalytics is done first
104-
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
105-
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
132+
const setQueryClauses = (queryClauses: any) => {
133+
if (queryClauses.length) {
134+
const filtered = filterAnalytics(analytics, queryClauses);
135+
setFilteredAnalytics({ active: true, items: filtered });
136+
} else {
137+
setFilteredAnalytics({ active: false, items: [] });
138+
}
139+
};
140+
141+
const filterList = () => {
142+
if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) {
143+
// trigger table filtering with query for job id to trigger table filter
144+
const query = EuiSearchBar.Query.parse(searchQueryText);
145+
let clauses: any = [];
146+
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
147+
clauses = query.ast.clauses;
148+
}
149+
setQueryClauses(clauses);
150+
} else {
151+
setQueryClauses([]);
152+
}
153+
};
154+
106155
useEffect(() => {
107156
if (selectedIdFromUrlInitialized === false && analytics.length > 0) {
108157
const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
@@ -116,9 +165,15 @@ export const DataFrameAnalyticsList: FC<Props> = ({
116165

117166
setSelectedIdFromUrlInitialized(true);
118167
setSearchQueryText(queryText);
168+
} else {
169+
filterList();
119170
}
120171
}, [selectedIdFromUrlInitialized, analytics]);
121172

173+
useEffect(() => {
174+
filterList();
175+
}, [selectedIdFromUrlInitialized, searchQueryText]);
176+
122177
const getAnalyticsCallback = useCallback(() => getAnalytics(true), []);
123178

124179
// Subscribe to the refresh observable to trigger reloading the analytics list.
@@ -137,6 +192,10 @@ export const DataFrameAnalyticsList: FC<Props> = ({
137192
isMlEnabledInSpace
138193
);
139194

195+
const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
196+
filteredAnalytics.active ? filteredAnalytics.items : analytics
197+
);
198+
140199
// Before the analytics have been loaded for the first time, display the loading indicator only.
141200
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
142201
if (!isInitialized) {
@@ -160,34 +219,10 @@ export const DataFrameAnalyticsList: FC<Props> = ({
160219
if (analytics.length === 0) {
161220
return (
162221
<>
163-
<EuiEmptyPrompt
164-
iconType="createAdvancedJob"
165-
title={
166-
<h2>
167-
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
168-
defaultMessage: 'Create your first data frame analytics job',
169-
})}
170-
</h2>
171-
}
172-
actions={
173-
!isManagementTable
174-
? [
175-
<EuiButton
176-
onClick={() => setIsSourceIndexModalVisible(true)}
177-
isDisabled={disabled}
178-
color="primary"
179-
iconType="plusInCircle"
180-
fill
181-
data-test-subj="mlAnalyticsCreateFirstButton"
182-
>
183-
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
184-
defaultMessage: 'Create job',
185-
})}
186-
</EuiButton>,
187-
]
188-
: []
189-
}
190-
data-test-subj="mlNoDataFrameAnalyticsFound"
222+
<AnalyticsEmptyPrompt
223+
isManagementTable={isManagementTable}
224+
disabled={disabled}
225+
onCreateFirstJobClick={() => setIsSourceIndexModalVisible(true)}
191226
/>
192227
{isSourceIndexModalVisible === true && (
193228
<SourceSelection onClose={() => setIsSourceIndexModalVisible(false)} />
@@ -196,95 +231,32 @@ export const DataFrameAnalyticsList: FC<Props> = ({
196231
);
197232
}
198233

199-
const sorting = {
200-
sort: {
201-
field: sortField,
202-
direction: sortDirection,
203-
},
204-
};
205-
206234
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, analytics);
207235

208-
const pagination = {
209-
initialPageIndex: pageIndex,
210-
initialPageSize: pageSize,
211-
totalItemCount: analytics.length,
212-
pageSizeOptions: [10, 20, 50],
213-
hidePerPageOptions: false,
214-
};
215-
216-
const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => {
217-
if (search.error !== null) {
218-
setSearchError(search.error.message);
219-
return false;
220-
}
221-
222-
setSearchError(undefined);
223-
setSearchQueryText(search.queryText);
224-
return true;
225-
};
226-
227-
const search: EuiSearchBarProps = {
228-
query: searchQueryText,
229-
onChange: handleSearchOnChange,
230-
box: {
231-
incremental: true,
232-
},
233-
filters: [
234-
{
235-
type: 'field_value_selection',
236-
field: 'job_type',
237-
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
238-
defaultMessage: 'Type',
239-
}),
240-
multiSelect: 'or',
241-
options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({
242-
value: val,
243-
name: val,
244-
view: getJobTypeBadge(val),
245-
})),
246-
},
247-
{
248-
type: 'field_value_selection',
249-
field: 'state',
250-
name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', {
251-
defaultMessage: 'Status',
252-
}),
253-
multiSelect: 'or',
254-
options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({
255-
value: val,
256-
name: val,
257-
view: getTaskStateBadge(val),
258-
})),
259-
},
260-
],
261-
};
262-
263-
const onTableChange: EuiInMemoryTable<DataFrameAnalyticsListRow>['onTableChange'] = ({
264-
page = { index: 0, size: 10 },
265-
sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
266-
}) => {
267-
const { index, size } = page;
268-
setPageIndex(index);
269-
setPageSize(size);
236+
const stats = analyticsStats && (
237+
<EuiFlexItem grow={false}>
238+
<StatsBar stats={analyticsStats} dataTestSub={'mlAnalyticsStatsBar'} />
239+
</EuiFlexItem>
240+
);
270241

271-
const { field, direction } = sort;
272-
setSortField(field);
273-
setSortDirection(direction);
274-
};
242+
const managementStats = (
243+
<EuiFlexItem>
244+
<EuiFlexGroup justifyContent="spaceBetween">
245+
{stats}
246+
<EuiFlexItem grow={false}>
247+
<RefreshAnalyticsListButton />
248+
</EuiFlexItem>
249+
</EuiFlexGroup>
250+
</EuiFlexItem>
251+
);
275252

276253
return (
277254
<>
278255
{modals}
279-
<EuiSpacer size="m" />
256+
{!isManagementTable && <EuiSpacer size="m" />}
280257
<EuiFlexGroup justifyContent="spaceBetween">
281-
<EuiFlexItem grow={false}>
282-
{analyticsStats && (
283-
<EuiFlexItem grow={false}>
284-
<StatsBar stats={analyticsStats} dataTestSub={'mlAnalyticsStatsBar'} />
285-
</EuiFlexItem>
286-
)}
287-
</EuiFlexItem>
258+
{!isManagementTable && stats}
259+
{isManagementTable && managementStats}
288260
<EuiFlexItem grow={false}>
289261
<EuiFlexGroup alignItems="center" gutterSize="s">
290262
{!isManagementTable && (
@@ -300,22 +272,25 @@ export const DataFrameAnalyticsList: FC<Props> = ({
300272
</EuiFlexGroup>
301273
<EuiSpacer size="m" />
302274
<div data-test-subj="mlAnalyticsTableContainer">
303-
<EuiInMemoryTable
304-
allowNeutralSort={false}
275+
<AnalyticsSearchBar
276+
filters={filters}
277+
searchQueryText={searchQueryText}
278+
setSearchQueryText={setSearchQueryText}
279+
/>
280+
<EuiSpacer size="l" />
281+
<EuiBasicTable<DataFrameAnalyticsListRow>
305282
className="mlAnalyticsTable"
306283
columns={columns}
307-
error={searchError}
308284
hasActions={false}
309285
isExpandable={true}
310286
isSelectable={false}
311-
items={analytics}
287+
items={pageOfItems}
312288
itemId={DataFrameAnalyticsListColumn.id}
313289
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
314290
loading={isLoading}
315-
onTableChange={onTableChange}
316-
pagination={pagination}
291+
onChange={onTableChange}
292+
pagination={pagination!}
317293
sorting={sorting}
318-
search={search}
319294
data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'}
320295
rowProps={(item) => ({
321296
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type Clause = Parameters<typeof Query['isMust']>[0];
2626
type ExtractClauseType<T> = T extends (x: any) => x is infer Type ? Type : never;
2727
export type TermClause = ExtractClauseType<typeof Ast['Term']['isInstance']>;
2828
export type FieldClause = ExtractClauseType<typeof Ast['Field']['isInstance']>;
29+
export type Value = Parameters<typeof Ast['Term']['must']>[0];
2930

3031
interface ProgressSection {
3132
phase: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { FC } from 'react';
8+
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
9+
import { i18n } from '@kbn/i18n';
10+
11+
interface Props {
12+
disabled: boolean;
13+
isManagementTable: boolean;
14+
onCreateFirstJobClick: () => void;
15+
}
16+
17+
export const AnalyticsEmptyPrompt: FC<Props> = ({
18+
disabled,
19+
isManagementTable,
20+
onCreateFirstJobClick,
21+
}) => (
22+
<EuiEmptyPrompt
23+
iconType="createAdvancedJob"
24+
title={
25+
<h2>
26+
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
27+
defaultMessage: 'Create your first data frame analytics job',
28+
})}
29+
</h2>
30+
}
31+
actions={
32+
!isManagementTable
33+
? [
34+
<EuiButton
35+
onClick={onCreateFirstJobClick}
36+
isDisabled={disabled}
37+
color="primary"
38+
iconType="plusInCircle"
39+
fill
40+
data-test-subj="mlAnalyticsCreateFirstButton"
41+
>
42+
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
43+
defaultMessage: 'Create job',
44+
})}
45+
</EuiButton>,
46+
]
47+
: []
48+
}
49+
data-test-subj="mlNoDataFrameAnalyticsFound"
50+
/>
51+
);

0 commit comments

Comments
 (0)