mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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
This commit is contained in:
parent
4762cf56f5
commit
70cea48718
8 changed files with 446 additions and 138 deletions
|
@ -5,17 +5,13 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
Direction,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTable,
|
||||
EuiSearchBar,
|
||||
EuiSearchBarProps,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
@ -43,6 +39,39 @@ import {
|
|||
getGroupQueryText,
|
||||
} from '../../../../../jobs/jobs_list/components/utils';
|
||||
import { SourceSelection } from '../source_selection';
|
||||
import { filterAnalytics, AnalyticsSearchBar } from '../analytics_search_bar';
|
||||
import { AnalyticsEmptyPrompt } from './empty_prompt';
|
||||
import { useTableSettings } from './use_table_settings';
|
||||
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
|
||||
|
||||
const filters: EuiSearchBarProps['filters'] = [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'job_type',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: getJobTypeBadge(val),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: getTaskStateBadge(val),
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
function getItemIdToExpandedRowMap(
|
||||
itemIds: DataFrameAnalyticsId[],
|
||||
|
@ -70,23 +99,23 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [filteredAnalytics, setFilteredAnalytics] = useState<{
|
||||
active: boolean;
|
||||
items: DataFrameAnalyticsListRow[];
|
||||
}>({
|
||||
active: false,
|
||||
items: [],
|
||||
});
|
||||
const [searchQueryText, setSearchQueryText] = useState('');
|
||||
|
||||
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<any>(undefined);
|
||||
const [searchError, setSearchError] = useState<any>(undefined);
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [sortField, setSortField] = useState<string>(DataFrameAnalyticsListColumn.id);
|
||||
const [sortDirection, setSortDirection] = useState<Direction>('asc');
|
||||
// Query text/job_id based on url but only after getAnalytics is done first
|
||||
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
|
||||
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
|
||||
|
||||
const disabled =
|
||||
!checkPermission('canCreateDataFrameAnalytics') ||
|
||||
|
@ -100,9 +129,29 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
blockRefresh
|
||||
);
|
||||
|
||||
// Query text/job_id based on url but only after getAnalytics is done first
|
||||
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
|
||||
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
|
||||
const setQueryClauses = (queryClauses: any) => {
|
||||
if (queryClauses.length) {
|
||||
const filtered = filterAnalytics(analytics, queryClauses);
|
||||
setFilteredAnalytics({ active: true, items: filtered });
|
||||
} else {
|
||||
setFilteredAnalytics({ active: false, items: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const filterList = () => {
|
||||
if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) {
|
||||
// trigger table filtering with query for job id to trigger table filter
|
||||
const query = EuiSearchBar.Query.parse(searchQueryText);
|
||||
let clauses: any = [];
|
||||
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
|
||||
clauses = query.ast.clauses;
|
||||
}
|
||||
setQueryClauses(clauses);
|
||||
} else {
|
||||
setQueryClauses([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIdFromUrlInitialized === false && analytics.length > 0) {
|
||||
const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
|
||||
|
@ -116,9 +165,15 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
|
||||
setSelectedIdFromUrlInitialized(true);
|
||||
setSearchQueryText(queryText);
|
||||
} else {
|
||||
filterList();
|
||||
}
|
||||
}, [selectedIdFromUrlInitialized, analytics]);
|
||||
|
||||
useEffect(() => {
|
||||
filterList();
|
||||
}, [selectedIdFromUrlInitialized, searchQueryText]);
|
||||
|
||||
const getAnalyticsCallback = useCallback(() => getAnalytics(true), []);
|
||||
|
||||
// Subscribe to the refresh observable to trigger reloading the analytics list.
|
||||
|
@ -137,6 +192,10 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
isMlEnabledInSpace
|
||||
);
|
||||
|
||||
const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
|
||||
filteredAnalytics.active ? filteredAnalytics.items : analytics
|
||||
);
|
||||
|
||||
// Before the analytics have been loaded for the first time, display the loading indicator only.
|
||||
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
|
||||
if (!isInitialized) {
|
||||
|
@ -160,34 +219,10 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
if (analytics.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<EuiEmptyPrompt
|
||||
iconType="createAdvancedJob"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
|
||||
defaultMessage: 'Create your first data frame analytics job',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
actions={
|
||||
!isManagementTable
|
||||
? [
|
||||
<EuiButton
|
||||
onClick={() => setIsSourceIndexModalVisible(true)}
|
||||
isDisabled={disabled}
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
data-test-subj="mlAnalyticsCreateFirstButton"
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
|
||||
defaultMessage: 'Create job',
|
||||
})}
|
||||
</EuiButton>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
data-test-subj="mlNoDataFrameAnalyticsFound"
|
||||
<AnalyticsEmptyPrompt
|
||||
isManagementTable={isManagementTable}
|
||||
disabled={disabled}
|
||||
onCreateFirstJobClick={() => setIsSourceIndexModalVisible(true)}
|
||||
/>
|
||||
{isSourceIndexModalVisible === true && (
|
||||
<SourceSelection onClose={() => setIsSourceIndexModalVisible(false)} />
|
||||
|
@ -196,95 +231,32 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, analytics);
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: analytics.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
const stats = analyticsStats && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatsBar stats={analyticsStats} dataTestSub={'mlAnalyticsStatsBar'} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => {
|
||||
if (search.error !== null) {
|
||||
setSearchError(search.error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
setSearchError(undefined);
|
||||
setSearchQueryText(search.queryText);
|
||||
return true;
|
||||
};
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
query: searchQueryText,
|
||||
onChange: handleSearchOnChange,
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'job_type',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: getJobTypeBadge(val),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: getTaskStateBadge(val),
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const onTableChange: EuiInMemoryTable<DataFrameAnalyticsListRow>['onTableChange'] = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
const managementStats = (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{stats}
|
||||
<EuiFlexItem grow={false}>
|
||||
<RefreshAnalyticsListButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modals}
|
||||
<EuiSpacer size="m" />
|
||||
{!isManagementTable && <EuiSpacer size="m" />}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
{analyticsStats && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatsBar stats={analyticsStats} dataTestSub={'mlAnalyticsStatsBar'} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{!isManagementTable && stats}
|
||||
{isManagementTable && managementStats}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{!isManagementTable && (
|
||||
|
@ -300,22 +272,25 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<div data-test-subj="mlAnalyticsTableContainer">
|
||||
<EuiInMemoryTable
|
||||
allowNeutralSort={false}
|
||||
<AnalyticsSearchBar
|
||||
filters={filters}
|
||||
searchQueryText={searchQueryText}
|
||||
setSearchQueryText={setSearchQueryText}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiBasicTable<DataFrameAnalyticsListRow>
|
||||
className="mlAnalyticsTable"
|
||||
columns={columns}
|
||||
error={searchError}
|
||||
hasActions={false}
|
||||
isExpandable={true}
|
||||
isSelectable={false}
|
||||
items={analytics}
|
||||
items={pageOfItems}
|
||||
itemId={DataFrameAnalyticsListColumn.id}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
loading={isLoading}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
pagination={pagination!}
|
||||
sorting={sorting}
|
||||
search={search}
|
||||
data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'}
|
||||
rowProps={(item) => ({
|
||||
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,
|
||||
|
|
|
@ -26,6 +26,7 @@ export type Clause = Parameters<typeof Query['isMust']>[0];
|
|||
type ExtractClauseType<T> = T extends (x: any) => x is infer Type ? Type : never;
|
||||
export type TermClause = ExtractClauseType<typeof Ast['Term']['isInstance']>;
|
||||
export type FieldClause = ExtractClauseType<typeof Ast['Field']['isInstance']>;
|
||||
export type Value = Parameters<typeof Ast['Term']['must']>[0];
|
||||
|
||||
interface ProgressSection {
|
||||
phase: string;
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
isManagementTable: boolean;
|
||||
onCreateFirstJobClick: () => void;
|
||||
}
|
||||
|
||||
export const AnalyticsEmptyPrompt: FC<Props> = ({
|
||||
disabled,
|
||||
isManagementTable,
|
||||
onCreateFirstJobClick,
|
||||
}) => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="createAdvancedJob"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
|
||||
defaultMessage: 'Create your first data frame analytics job',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
actions={
|
||||
!isManagementTable
|
||||
? [
|
||||
<EuiButton
|
||||
onClick={onCreateFirstJobClick}
|
||||
isDisabled={disabled}
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
data-test-subj="mlAnalyticsCreateFirstButton"
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
|
||||
defaultMessage: 'Create job',
|
||||
})}
|
||||
</EuiButton>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
data-test-subj="mlNoDataFrameAnalyticsFound"
|
||||
/>
|
||||
);
|
|
@ -23,7 +23,6 @@ import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url';
|
|||
|
||||
import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common';
|
||||
import {
|
||||
getDataFrameAnalyticsProgress,
|
||||
getDataFrameAnalyticsProgressPhase,
|
||||
isDataFrameAnalyticsFailed,
|
||||
isDataFrameAnalyticsRunning,
|
||||
|
@ -76,7 +75,6 @@ export const progressColumn = {
|
|||
name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', {
|
||||
defaultMessage: 'Progress',
|
||||
}),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats),
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
const { currentPhase, progress, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats);
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import get from 'lodash/get';
|
||||
import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50];
|
||||
|
||||
const jobPropertyMap = {
|
||||
ID: 'id',
|
||||
Status: 'state',
|
||||
Type: 'job_type',
|
||||
};
|
||||
|
||||
interface AnalyticsBasicTableSettings {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
totalItemCount: number;
|
||||
hidePerPageOptions: boolean;
|
||||
sortField: string;
|
||||
sortDirection: Direction;
|
||||
}
|
||||
|
||||
interface UseTableSettingsReturnValue {
|
||||
onTableChange: EuiBasicTableProps<DataFrameAnalyticsListRow>['onChange'];
|
||||
pageOfItems: DataFrameAnalyticsListRow[];
|
||||
pagination: EuiBasicTableProps<DataFrameAnalyticsListRow>['pagination'];
|
||||
sorting: EuiTableSortingType<any>;
|
||||
}
|
||||
|
||||
export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue {
|
||||
const [tableSettings, setTableSettings] = useState<AnalyticsBasicTableSettings>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount: 0,
|
||||
hidePerPageOptions: false,
|
||||
sortField: DataFrameAnalyticsListColumn.id,
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
|
||||
const getPageOfItems = (
|
||||
list: any[],
|
||||
index: number,
|
||||
size: number,
|
||||
sortField: string,
|
||||
sortDirection: Direction
|
||||
) => {
|
||||
list = sortBy(list, (item) =>
|
||||
get(item, jobPropertyMap[sortField as keyof typeof jobPropertyMap] || sortField)
|
||||
);
|
||||
list = sortDirection === 'asc' ? list : list.reverse();
|
||||
const listLength = list.length;
|
||||
|
||||
let pageStart = index * size;
|
||||
if (pageStart >= listLength && listLength !== 0) {
|
||||
// if the page start is larger than the number of items due to
|
||||
// filters being applied or items being deleted, calculate a new page start
|
||||
pageStart = Math.floor((listLength - 1) / size) * size;
|
||||
|
||||
setTableSettings({ ...tableSettings, pageIndex: pageStart / size });
|
||||
}
|
||||
return {
|
||||
pageOfItems: list.slice(pageStart, pageStart + size),
|
||||
totalItemCount: listLength,
|
||||
};
|
||||
};
|
||||
|
||||
const onTableChange = ({
|
||||
page = { index: 0, size: PAGE_SIZE },
|
||||
sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
|
||||
}: {
|
||||
page?: { index: number; size: number };
|
||||
sort?: { field: string; direction: Direction };
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
const { field, direction } = sort;
|
||||
|
||||
setTableSettings({
|
||||
...tableSettings,
|
||||
pageIndex: index,
|
||||
pageSize: size,
|
||||
sortField: field,
|
||||
sortDirection: direction,
|
||||
});
|
||||
};
|
||||
|
||||
const { pageIndex, pageSize, sortField, sortDirection } = tableSettings;
|
||||
|
||||
const { pageOfItems, totalItemCount } = getPageOfItems(
|
||||
items,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection
|
||||
);
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return { onTableChange, pageOfItems, pagination, sorting };
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Dispatch, SetStateAction, FC, Fragment, useState } from 'react';
|
||||
import {
|
||||
EuiSearchBar,
|
||||
EuiSearchBarProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { stringMatch } from '../../../../../util/string_utils';
|
||||
import {
|
||||
TermClause,
|
||||
FieldClause,
|
||||
Value,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../analytics_list/common';
|
||||
|
||||
export function filterAnalytics(
|
||||
items: DataFrameAnalyticsListRow[],
|
||||
clauses: Array<TermClause | FieldClause>
|
||||
) {
|
||||
if (clauses.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// keep count of the number of matches we make as we're looping over the clauses
|
||||
// we only want to return items which match all clauses, i.e. each search term is ANDed
|
||||
const matches: Record<string, any> = items.reduce((p: Record<string, any>, c) => {
|
||||
p[c.id] = {
|
||||
job: c,
|
||||
count: 0,
|
||||
};
|
||||
return p;
|
||||
}, {});
|
||||
|
||||
clauses.forEach((c) => {
|
||||
// the search term could be negated with a minus, e.g. -bananas
|
||||
const bool = c.match === 'must';
|
||||
let js = [];
|
||||
|
||||
if (c.type === 'term') {
|
||||
// filter term based clauses, e.g. bananas
|
||||
// match on id, description and memory_status
|
||||
// if the term has been negated, AND the matches
|
||||
if (bool === true) {
|
||||
js = items.filter(
|
||||
(item) =>
|
||||
stringMatch(item.id, c.value) === bool ||
|
||||
stringMatch(item.config.description, c.value) === bool ||
|
||||
stringMatch(item.stats?.memory_usage?.status, c.value) === bool
|
||||
);
|
||||
} else {
|
||||
js = items.filter(
|
||||
(item) =>
|
||||
stringMatch(item.id, c.value) === bool &&
|
||||
stringMatch(item.config.description, c.value) === bool &&
|
||||
stringMatch(item.stats?.memory_usage?.status, c.value) === bool
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// filter other clauses, i.e. the filters for type and status
|
||||
if (Array.isArray(c.value)) {
|
||||
// job type value and status value are an array of string(s) e.g. c.value => ['failed', 'stopped']
|
||||
js = items.filter((item) =>
|
||||
(c.value as Value[]).includes(
|
||||
item[c.field as keyof Pick<typeof item, 'job_type' | 'state'>]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
js = items.filter(
|
||||
(item) => item[c.field as keyof Pick<typeof item, 'job_type' | 'state'>] === c.value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
js.forEach((j) => matches[j.id].count++);
|
||||
});
|
||||
|
||||
// loop through the matches and return only those items which have match all the clauses
|
||||
const filtered = Object.values(matches)
|
||||
.filter((m) => (m && m.count) >= clauses.length)
|
||||
.map((m) => m.job);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getError(errorMessage: string | null) {
|
||||
if (errorMessage) {
|
||||
return i18n.translate('xpack.ml.analyticList.searchBar.invalidSearchErrorMessage', {
|
||||
defaultMessage: 'Invalid search: {errorMessage}',
|
||||
values: { errorMessage },
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filters: EuiSearchBarProps['filters'];
|
||||
searchQueryText: string;
|
||||
setSearchQueryText: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const AnalyticsSearchBar: FC<Props> = ({ filters, searchQueryText, setSearchQueryText }) => {
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
|
||||
const onChange: EuiSearchBarProps['onChange'] = ({ query, error }) => {
|
||||
if (error) {
|
||||
setErrorMessage(error.message);
|
||||
} else if (query !== null && query.text !== undefined) {
|
||||
setSearchQueryText(query.text);
|
||||
setErrorMessage(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem data-test-subj="mlAnalyticsSearchBar" grow={false}>
|
||||
{searchQueryText === undefined && (
|
||||
<EuiSearchBar
|
||||
box={{
|
||||
incremental: true,
|
||||
}}
|
||||
filters={filters}
|
||||
onChange={onChange}
|
||||
className="mlAnalyitcsSearchBar"
|
||||
/>
|
||||
)}
|
||||
{searchQueryText !== undefined && (
|
||||
<EuiSearchBar
|
||||
box={{
|
||||
incremental: true,
|
||||
}}
|
||||
defaultQuery={searchQueryText}
|
||||
filters={filters}
|
||||
onChange={onChange}
|
||||
className="mlAnalyitcsSearchBar"
|
||||
/>
|
||||
)}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={errorMessage !== null}
|
||||
error={getError(errorMessage)}
|
||||
style={{ maxHeight: '0px' }}
|
||||
>
|
||||
<Fragment />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar';
|
|
@ -7,7 +7,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import moment from 'moment';
|
||||
|
||||
import { toLocaleString } from '../../../../util/string_utils';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue