mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Fixes handling of built-in models (#92154)
* [ML] add description column and details tab * [ML] restrict build-in models actions * [ML] add description to the details tab * [ML] add flex with wrap to the type column * [ML] remove unused code for filtering
This commit is contained in:
parent
12bb59abb0
commit
a75abd7e41
6 changed files with 129 additions and 175 deletions
|
@ -29,4 +29,6 @@ export const JOB_MAP_NODE_TYPES = {
|
|||
TRAINED_MODEL: 'trainedModel',
|
||||
} as const;
|
||||
|
||||
export const BUILT_IN_MODEL_TAG = 'prepackaged';
|
||||
|
||||
export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES];
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface TrainedModelStat {
|
|||
}
|
||||
|
||||
export interface TrainedModelConfigResponse {
|
||||
description: string;
|
||||
created_by: string;
|
||||
create_time: string;
|
||||
default_field_map: Record<string, string>;
|
||||
|
@ -61,7 +62,7 @@ export interface TrainedModelConfigResponse {
|
|||
}
|
||||
| Record<string, any>;
|
||||
model_id: string;
|
||||
tags: string;
|
||||
tags: string[];
|
||||
version: string;
|
||||
inference_config?: Record<string, any>;
|
||||
pipelines?: Record<string, PipelineDefinition> | null;
|
||||
|
|
|
@ -12,68 +12,6 @@ import {
|
|||
Value,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../pages/analytics_management/components/analytics_list/common';
|
||||
import { ModelItem } from '../pages/analytics_management/components/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[],
|
||||
|
|
|
@ -66,9 +66,11 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
license_level,
|
||||
pipelines,
|
||||
description,
|
||||
} = item;
|
||||
|
||||
const details = {
|
||||
description,
|
||||
tags,
|
||||
version,
|
||||
estimated_operations,
|
||||
|
@ -104,8 +106,8 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
),
|
||||
};
|
||||
})
|
||||
.filter(({ description }) => {
|
||||
return description !== undefined;
|
||||
.filter(({ description: d }) => {
|
||||
return d !== undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -365,62 +367,64 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiFlexGrid columns={2} gutterSize={'m'}>
|
||||
{Object.entries(pipelines).map(([pipelineName, { processors, description }]) => {
|
||||
return (
|
||||
<EuiFlexItem key={pipelineName}>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>{pipelineName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={async () => {
|
||||
const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator(
|
||||
'INGEST_PIPELINES_APP_URL_GENERATOR'
|
||||
);
|
||||
await navigateToUrl(
|
||||
await ingestPipelinesAppUrlGenerator.createUrl({
|
||||
page: 'pipeline_edit',
|
||||
pipelineId: pipelineName,
|
||||
absolute: true,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.editPipelineLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{Object.entries(pipelines).map(
|
||||
([pipelineName, { processors, description: pipelineDescription }]) => {
|
||||
return (
|
||||
<EuiFlexItem key={pipelineName}>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>{pipelineName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={async () => {
|
||||
const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator(
|
||||
'INGEST_PIPELINES_APP_URL_GENERATOR'
|
||||
);
|
||||
await navigateToUrl(
|
||||
await ingestPipelinesAppUrlGenerator.createUrl({
|
||||
page: 'pipeline_edit',
|
||||
pipelineId: pipelineName,
|
||||
absolute: true,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.editPipelineLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{description && <EuiText>{description}</EuiText>}
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiTitle size={'xxs'}>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.processorsTitle"
|
||||
defaultMessage="Processors"
|
||||
/>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock
|
||||
language="painless"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{JSON.stringify(processors, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
{pipelineDescription && <EuiText>{pipelineDescription}</EuiText>}
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiTitle size={'xxs'}>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.processorsTitle"
|
||||
defaultMessage="Processors"
|
||||
/>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock
|
||||
language="painless"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{JSON.stringify(processors, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
),
|
||||
|
|
|
@ -9,6 +9,7 @@ export * from './models_list';
|
|||
|
||||
export const ModelsTableToConfigMapping = {
|
||||
id: 'model_id',
|
||||
description: 'description',
|
||||
createdAt: 'create_time',
|
||||
type: 'type',
|
||||
} as const;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { FC, useState, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
|
@ -14,7 +14,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiBadge,
|
||||
|
@ -48,18 +47,17 @@ import {
|
|||
refreshAnalyticsList$,
|
||||
useRefreshAnalyticsList,
|
||||
} from '../../../../common';
|
||||
import { useTableSettings } from '../analytics_list/use_table_settings';
|
||||
import { filterAnalyticsModels } from '../../../../common/search_bar_filters';
|
||||
import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
|
||||
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
|
||||
import { timeFormatter } from '../../../../../../../common/util/date_utils';
|
||||
import { ListingPageUrlState } from '../../../../../../../common/types/common';
|
||||
import { usePageUrlState } from '../../../../../util/url_state';
|
||||
import { BUILT_IN_MODEL_TAG } from '../../../../../../../common/constants/data_frame_analytics';
|
||||
|
||||
type Stats = Omit<TrainedModelStat, 'model_id'>;
|
||||
|
||||
export type ModelItem = TrainedModelConfigResponse & {
|
||||
type?: string;
|
||||
type?: string[];
|
||||
stats?: Stats;
|
||||
pipelines?: ModelPipelines['pipelines'] | null;
|
||||
};
|
||||
|
@ -73,6 +71,11 @@ export const getDefaultModelsListState = (): ListingPageUrlState => ({
|
|||
sortDirection: 'asc',
|
||||
});
|
||||
|
||||
export const BUILT_IN_MODEL_TYPE = i18n.translate(
|
||||
'xpack.ml.trainedModels.modelsList.builtInModelLabel',
|
||||
{ defaultMessage: 'built-in' }
|
||||
);
|
||||
|
||||
export const ModelsList: FC = () => {
|
||||
const {
|
||||
services: {
|
||||
|
@ -99,7 +102,6 @@ export const ModelsList: FC = () => {
|
|||
const trainedModelsApiService = useTrainedModelsApiService();
|
||||
const { toasts } = useNotifications();
|
||||
|
||||
const [filteredModels, setFilteredModels] = useState<ModelItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [items, setItems] = useState<ModelItem[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<ModelItem[]>([]);
|
||||
|
@ -111,36 +113,15 @@ export const ModelsList: FC = () => {
|
|||
const mlUrlGenerator = useMlUrlGenerator();
|
||||
const navigateToPath = useNavigateToPath();
|
||||
|
||||
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]);
|
||||
const isBuiltInModel = useCallback(
|
||||
(item: ModelItem) => item.tags.includes(BUILT_IN_MODEL_TAG),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetches inference trained models.
|
||||
* Fetches trained models.
|
||||
*/
|
||||
const fetchData = useCallback(async () => {
|
||||
const fetchModelsData = useCallback(async () => {
|
||||
try {
|
||||
const response = await trainedModelsApiService.getTrainedModels(undefined, {
|
||||
with_pipelines: true,
|
||||
|
@ -151,10 +132,16 @@ export const ModelsList: FC = () => {
|
|||
const expandedItemsToRefresh = [];
|
||||
|
||||
for (const model of response) {
|
||||
const tableItem = {
|
||||
const tableItem: ModelItem = {
|
||||
...model,
|
||||
// Extract model types
|
||||
...(typeof model.inference_config === 'object'
|
||||
? { type: Object.keys(model.inference_config)[0] }
|
||||
? {
|
||||
type: [
|
||||
...Object.keys(model.inference_config),
|
||||
...(isBuiltInModel(model) ? [BUILT_IN_MODEL_TYPE] : []),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
newItems.push(tableItem);
|
||||
|
@ -190,7 +177,7 @@ export const ModelsList: FC = () => {
|
|||
// Subscribe to the refresh observable to trigger reloading the model list.
|
||||
useRefreshAnalyticsList({
|
||||
isLoading: setIsLoading,
|
||||
onRefresh: fetchData,
|
||||
onRefresh: fetchModelsData,
|
||||
});
|
||||
|
||||
const modelsStats: ModelsBarStats = useMemo(() => {
|
||||
|
@ -369,7 +356,7 @@ export const ModelsList: FC = () => {
|
|||
onClick: async (model) => {
|
||||
await prepareModelsForDeletion([model]);
|
||||
},
|
||||
available: (item) => canDeleteDataFrameAnalytics,
|
||||
available: (item) => canDeleteDataFrameAnalytics && !isBuiltInModel(item),
|
||||
enabled: (item) => {
|
||||
// TODO check for permissions to delete ingest pipelines.
|
||||
// ATM undefined means pipelines fetch failed server-side.
|
||||
|
@ -418,6 +405,15 @@ export const ModelsList: FC = () => {
|
|||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.description,
|
||||
width: '350px',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelDescriptionHeader', {
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.type,
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.typeHeader', {
|
||||
|
@ -425,7 +421,15 @@ export const ModelsList: FC = () => {
|
|||
}),
|
||||
sortable: true,
|
||||
align: 'left',
|
||||
render: (type: string) => <EuiBadge color="hollow">{type}</EuiBadge>,
|
||||
render: (types: string[]) => (
|
||||
<EuiFlexGroup gutterSize={'xs'} wrap>
|
||||
{types.map((type) => (
|
||||
<EuiFlexItem key={type} grow={false}>
|
||||
<EuiBadge color="hollow">{type}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.createdAt,
|
||||
|
@ -459,12 +463,6 @@ export const ModelsList: FC = () => {
|
|||
]
|
||||
: [];
|
||||
|
||||
const { onTableChange, pagination, sorting } = useTableSettings<ModelItem>(
|
||||
filteredModels,
|
||||
pageState,
|
||||
updatePageState
|
||||
);
|
||||
|
||||
const toolsLeft = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
|
@ -496,15 +494,27 @@ export const ModelsList: FC = () => {
|
|||
const selection: EuiTableSelectionType<ModelItem> | undefined = isSelectionAllowed
|
||||
? {
|
||||
selectableMessage: (selectable, item) => {
|
||||
return selectable
|
||||
? i18n.translate('xpack.ml.trainedModels.modelsList.selectableMessage', {
|
||||
defaultMessage: 'Select a model',
|
||||
})
|
||||
: i18n.translate('xpack.ml.trainedModels.modelsList.disableSelectableMessage', {
|
||||
defaultMessage: 'Model has associated pipelines',
|
||||
});
|
||||
if (selectable) {
|
||||
return i18n.translate('xpack.ml.trainedModels.modelsList.selectableMessage', {
|
||||
defaultMessage: 'Select a model',
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(item.pipelines) && item.pipelines.length > 0) {
|
||||
return i18n.translate('xpack.ml.trainedModels.modelsList.disableSelectableMessage', {
|
||||
defaultMessage: 'Model has associated pipelines',
|
||||
});
|
||||
}
|
||||
|
||||
if (isBuiltInModel(item)) {
|
||||
return i18n.translate('xpack.ml.trainedModels.modelsList.builtInModelMessage', {
|
||||
defaultMessage: 'Built-in model',
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
selectable: (item) => !item.pipelines,
|
||||
selectable: (item) => !item.pipelines && !isBuiltInModel(item),
|
||||
onSelectionChange: (selectedItems) => {
|
||||
setSelectedModels(selectedItems);
|
||||
},
|
||||
|
@ -534,6 +544,7 @@ export const ModelsList: FC = () => {
|
|||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -556,9 +567,6 @@ export const ModelsList: FC = () => {
|
|||
items={items}
|
||||
itemId={ModelsTableToConfigMapping.id}
|
||||
loading={isLoading}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
search={search}
|
||||
selection={selection}
|
||||
rowProps={(item) => ({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue