mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] DF Analytics models list: persist pagination through refresh interval (#76695)
* ensure wizard error shows up correctly * wip: switch to basic table for model management * add selection and multi job action to models list * update error extraction function * use generic types in hook * simplify filtered items
This commit is contained in:
parent
b149a60554
commit
8bc6898c33
7 changed files with 210 additions and 139 deletions
|
@ -135,7 +135,14 @@ export const extractErrorProperties = (
|
|||
typeof error.body.attributes === 'object' &&
|
||||
error.body.attributes.body?.status !== undefined
|
||||
) {
|
||||
statusCode = error.body.attributes.body?.status;
|
||||
statusCode = error.body.attributes.body.status;
|
||||
|
||||
if (typeof error.body.attributes.body.error?.reason === 'string') {
|
||||
return {
|
||||
message: error.body.attributes.body.error.reason,
|
||||
statusCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof error.body.message === 'string') {
|
||||
|
|
|
@ -209,7 +209,6 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
|
|||
let unsupportedFieldsErrorMessage;
|
||||
if (
|
||||
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
|
||||
errorMessage.includes('status_exception') &&
|
||||
(errorMessage.includes('must have at most') || errorMessage.includes('must have at least'))
|
||||
) {
|
||||
maxDistinctValuesErrorMessage = errorMessage;
|
||||
|
|
|
@ -99,13 +99,7 @@ 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 [filteredAnalytics, setFilteredAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [searchQueryText, setSearchQueryText] = useState('');
|
||||
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
|
||||
|
@ -129,12 +123,12 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
blockRefresh
|
||||
);
|
||||
|
||||
const setQueryClauses = (queryClauses: any) => {
|
||||
const updateFilteredItems = (queryClauses: any) => {
|
||||
if (queryClauses.length) {
|
||||
const filtered = filterAnalytics(analytics, queryClauses);
|
||||
setFilteredAnalytics({ active: true, items: filtered });
|
||||
setFilteredAnalytics(filtered);
|
||||
} else {
|
||||
setFilteredAnalytics({ active: false, items: [] });
|
||||
setFilteredAnalytics(analytics);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -146,9 +140,9 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
|
||||
clauses = query.ast.clauses;
|
||||
}
|
||||
setQueryClauses(clauses);
|
||||
updateFilteredItems(clauses);
|
||||
} else {
|
||||
setQueryClauses([]);
|
||||
updateFilteredItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -192,9 +186,9 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
isMlEnabledInSpace
|
||||
);
|
||||
|
||||
const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
|
||||
filteredAnalytics.active ? filteredAnalytics.items : analytics
|
||||
);
|
||||
const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings<
|
||||
DataFrameAnalyticsListRow
|
||||
>(DataFrameAnalyticsListColumn.id, filteredAnalytics);
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -8,7 +8,6 @@ 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];
|
||||
|
@ -19,37 +18,59 @@ const jobPropertyMap = {
|
|||
Type: 'job_type',
|
||||
};
|
||||
|
||||
interface AnalyticsBasicTableSettings {
|
||||
// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange
|
||||
// Can be removed when https://github.com/elastic/eui/issues/4011 is addressed in EUI
|
||||
export interface Criteria<T> {
|
||||
page?: {
|
||||
index: number;
|
||||
size: number;
|
||||
};
|
||||
sort?: {
|
||||
field: keyof T;
|
||||
direction: Direction;
|
||||
};
|
||||
}
|
||||
export interface CriteriaWithPagination<T> extends Criteria<T> {
|
||||
page: {
|
||||
index: number;
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AnalyticsBasicTableSettings<T> {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
totalItemCount: number;
|
||||
hidePerPageOptions: boolean;
|
||||
sortField: string;
|
||||
sortField: keyof T;
|
||||
sortDirection: Direction;
|
||||
}
|
||||
|
||||
interface UseTableSettingsReturnValue {
|
||||
onTableChange: EuiBasicTableProps<DataFrameAnalyticsListRow>['onChange'];
|
||||
pageOfItems: DataFrameAnalyticsListRow[];
|
||||
pagination: EuiBasicTableProps<DataFrameAnalyticsListRow>['pagination'];
|
||||
interface UseTableSettingsReturnValue<T> {
|
||||
onTableChange: EuiBasicTableProps<T>['onChange'];
|
||||
pageOfItems: T[];
|
||||
pagination: EuiBasicTableProps<T>['pagination'];
|
||||
sorting: EuiTableSortingType<any>;
|
||||
}
|
||||
|
||||
export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue {
|
||||
const [tableSettings, setTableSettings] = useState<AnalyticsBasicTableSettings>({
|
||||
export function useTableSettings<TypeOfItem>(
|
||||
sortByField: keyof TypeOfItem,
|
||||
items: TypeOfItem[]
|
||||
): UseTableSettingsReturnValue<TypeOfItem> {
|
||||
const [tableSettings, setTableSettings] = useState<AnalyticsBasicTableSettings<TypeOfItem>>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount: 0,
|
||||
hidePerPageOptions: false,
|
||||
sortField: DataFrameAnalyticsListColumn.id,
|
||||
sortField: sortByField,
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
|
||||
const getPageOfItems = (
|
||||
list: any[],
|
||||
list: TypeOfItem[],
|
||||
index: number,
|
||||
size: number,
|
||||
sortField: string,
|
||||
sortField: keyof TypeOfItem,
|
||||
sortDirection: Direction
|
||||
) => {
|
||||
list = sortBy(list, (item) =>
|
||||
|
@ -72,13 +93,10 @@ export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSe
|
|||
};
|
||||
};
|
||||
|
||||
const onTableChange = ({
|
||||
const onTableChange: EuiBasicTableProps<TypeOfItem>['onChange'] = ({
|
||||
page = { index: 0, size: PAGE_SIZE },
|
||||
sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
|
||||
}: {
|
||||
page?: { index: number; size: number };
|
||||
sort?: { field: string; direction: Direction };
|
||||
}) => {
|
||||
sort = { field: sortByField, direction: 'asc' },
|
||||
}: CriteriaWithPagination<TypeOfItem>) => {
|
||||
const { index, size } = page;
|
||||
const { field, direction } = sort;
|
||||
|
||||
|
|
|
@ -20,6 +20,68 @@ import {
|
|||
Value,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../analytics_list/common';
|
||||
import { ModelItem } from '../models_management/models_list';
|
||||
|
||||
export function filterAnalyticsModels(
|
||||
items: ModelItem[],
|
||||
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.model_id] = {
|
||||
model: 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 ms = [];
|
||||
|
||||
if (c.type === 'term') {
|
||||
// filter term based clauses, e.g. bananas
|
||||
// match on model_id and type
|
||||
// if the term has been negated, AND the matches
|
||||
if (bool === true) {
|
||||
ms = items.filter(
|
||||
(item) =>
|
||||
stringMatch(item.model_id, c.value) === bool || stringMatch(item.type, c.value) === bool
|
||||
);
|
||||
} else {
|
||||
ms = items.filter(
|
||||
(item) =>
|
||||
stringMatch(item.model_id, c.value) === bool && stringMatch(item.type, c.value) === bool
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// filter other clauses, i.e. the filters for type
|
||||
if (Array.isArray(c.value)) {
|
||||
// type value is an array of string(s) e.g. c.value => ['classification']
|
||||
ms = items.filter((item) => {
|
||||
return item.type !== undefined && (c.value as Value[]).includes(item.type);
|
||||
});
|
||||
} else {
|
||||
ms = items.filter((item) => item[c.field as keyof typeof item] === c.value);
|
||||
}
|
||||
}
|
||||
|
||||
ms.forEach((j) => matches[j.model_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.model);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export function filterAnalytics(
|
||||
items: DataFrameAnalyticsListRow[],
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar';
|
||||
export { AnalyticsSearchBar, filterAnalytics, filterAnalyticsModels } from './analytics_search_bar';
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useCallback, useMemo } from 'react';
|
||||
import React, { FC, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
Direction,
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiSearchBarProps,
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiBadge,
|
||||
SearchFilterConfig,
|
||||
} from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { formatDate } from '@elastic/eui/lib/services/format';
|
||||
|
@ -42,6 +42,8 @@ import {
|
|||
refreshAnalyticsList$,
|
||||
useRefreshAnalyticsList,
|
||||
} from '../../../../common';
|
||||
import { useTableSettings } from '../analytics_list/use_table_settings';
|
||||
import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar';
|
||||
|
||||
type Stats = Omit<TrainedModelStat, 'model_id'>;
|
||||
|
||||
|
@ -66,22 +68,41 @@ export const ModelsList: FC = () => {
|
|||
const { toasts } = useNotifications();
|
||||
|
||||
const [searchQueryText, setSearchQueryText] = useState('');
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortField, setSortField] = useState<string>(ModelsTableToConfigMapping.id);
|
||||
const [sortDirection, setSortDirection] = useState<Direction>('asc');
|
||||
|
||||
const [filteredModels, setFilteredModels] = useState<ModelItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [items, setItems] = useState<ModelItem[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<ModelItem[]>([]);
|
||||
|
||||
const [modelsToDelete, setModelsToDelete] = useState<ModelItemFull[]>([]);
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const updateFilteredItems = (queryClauses: any) => {
|
||||
if (queryClauses.length) {
|
||||
const filtered = filterAnalyticsModels(items, queryClauses);
|
||||
setFilteredModels(filtered);
|
||||
} else {
|
||||
setFilteredModels(items);
|
||||
}
|
||||
};
|
||||
|
||||
const filterList = () => {
|
||||
if (searchQueryText !== '') {
|
||||
const query = EuiSearchBar.Query.parse(searchQueryText);
|
||||
let clauses: any = [];
|
||||
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
|
||||
clauses = query.ast.clauses;
|
||||
}
|
||||
updateFilteredItems(clauses);
|
||||
} else {
|
||||
updateFilteredItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
filterList();
|
||||
}, [searchQueryText, items]);
|
||||
|
||||
/**
|
||||
* Fetches inference trained models.
|
||||
*/
|
||||
|
@ -355,91 +376,51 @@ export const ModelsList: FC = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: items.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
const filters: SearchFilterConfig[] =
|
||||
inferenceTypesOptions && inferenceTypesOptions.length > 0
|
||||
? [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: inferenceTypesOptions,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
const search: EuiSearchBarProps = {
|
||||
query: searchQueryText,
|
||||
onChange: (searchChange) => {
|
||||
if (searchChange.error !== null) {
|
||||
return false;
|
||||
}
|
||||
setSearchQueryText(searchChange.queryText);
|
||||
return true;
|
||||
},
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
...(inferenceTypesOptions && inferenceTypesOptions.length > 0
|
||||
? {
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: inferenceTypesOptions,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
...(selectedModels.length > 0
|
||||
? {
|
||||
toolsLeft: (
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="s">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.inference.modelsList.selectedModelsMessage"
|
||||
defaultMessage="{modelsCount, plural, one{# model} other {# models}} selected"
|
||||
values={{ modelsCount: selectedModels.length }}
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={prepareModelsForDeletion.bind(null, selectedModels)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.inference.modelsList.deleteModelsButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings<ModelItem>(
|
||||
ModelsTableToConfigMapping.id,
|
||||
filteredModels
|
||||
);
|
||||
|
||||
const onTableChange: EuiInMemoryTable<ModelItem>['onTableChange'] = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: ModelsTableToConfigMapping.id, direction: 'asc' },
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
const toolsLeft = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="s">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.inference.modelsList.selectedModelsMessage"
|
||||
defaultMessage="{modelsCount, plural, one{# model} other {# models}} selected"
|
||||
values={{ modelsCount: selectedModels.length }}
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton color="danger" onClick={prepareModelsForDeletion.bind(null, selectedModels)}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.inference.modelsList.deleteModelsButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
const isSelectionAllowed = canDeleteDataFrameAnalytics;
|
||||
|
||||
|
@ -473,21 +454,31 @@ export const ModelsList: FC = () => {
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<div data-test-subj="mlModelsTableContainer">
|
||||
<EuiInMemoryTable
|
||||
allowNeutralSort={false}
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{selectedModels.length > 0 && toolsLeft}
|
||||
<EuiFlexItem>
|
||||
<AnalyticsSearchBar
|
||||
filters={filters}
|
||||
searchQueryText={searchQueryText}
|
||||
setSearchQueryText={setSearchQueryText}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiBasicTable<ModelItem>
|
||||
columns={columns}
|
||||
hasActions={true}
|
||||
isExpandable={true}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isSelectable={false}
|
||||
items={items}
|
||||
items={pageOfItems}
|
||||
itemId={ModelsTableToConfigMapping.id}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
loading={isLoading}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
search={search}
|
||||
onChange={onTableChange}
|
||||
selection={selection}
|
||||
pagination={pagination!}
|
||||
sorting={sorting}
|
||||
data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
|
||||
rowProps={(item) => ({
|
||||
'data-test-subj': `mlModelsTableRow row-${item.model_id}`,
|
||||
})}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue