diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 7d71819de5366..f13537892875d 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -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]; diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index 057ec5cc79d98..6b320d503b4c0 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -46,6 +46,7 @@ export interface TrainedModelStat { } export interface TrainedModelConfigResponse { + description: string; created_by: string; create_time: string; default_field_map: Record; @@ -61,7 +62,7 @@ export interface TrainedModelConfigResponse { } | Record; model_id: string; - tags: string; + tags: string[]; version: string; inference_config?: Record; pipelines?: Record | null; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/search_bar_filters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/common/search_bar_filters.tsx index df935cfdf3825..11754007da775 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/search_bar_filters.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/search_bar_filters.tsx @@ -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 -) { - 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 = items.reduce((p: Record, 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[], diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 626ba3dbc6baa..5562fcbe093a1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -66,9 +66,11 @@ export const ExpandedRow: FC = ({ 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 = ({ item }) => { ), }; }) - .filter(({ description }) => { - return description !== undefined; + .filter(({ description: d }) => { + return d !== undefined; }); } @@ -365,62 +367,64 @@ export const ExpandedRow: FC = ({ item }) => { <> - {Object.entries(pipelines).map(([pipelineName, { processors, description }]) => { - return ( - - - - - -
{pipelineName}
-
-
- - { - const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator( - 'INGEST_PIPELINES_APP_URL_GENERATOR' - ); - await navigateToUrl( - await ingestPipelinesAppUrlGenerator.createUrl({ - page: 'pipeline_edit', - pipelineId: pipelineName, - absolute: true, - }) - ); - }} - > + {Object.entries(pipelines).map( + ([pipelineName, { processors, description: pipelineDescription }]) => { + return ( + + + + + +
{pipelineName}
+
+
+ + { + const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator( + 'INGEST_PIPELINES_APP_URL_GENERATOR' + ); + await navigateToUrl( + await ingestPipelinesAppUrlGenerator.createUrl({ + page: 'pipeline_edit', + pipelineId: pipelineName, + absolute: true, + }) + ); + }} + > + + + +
+ + {pipelineDescription && {pipelineDescription}} + + +
- - - - - {description && {description}} - - -
- -
-
- - {JSON.stringify(processors, null, 2)} - - - - ); - })} +
+
+ + {JSON.stringify(processors, null, 2)} + +
+
+ ); + } + )}
), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts index ce67d265d7471..27c378aaed25b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts @@ -9,6 +9,7 @@ export * from './models_list'; export const ModelsTableToConfigMapping = { id: 'model_id', + description: 'description', createdAt: 'create_time', type: 'type', } as const; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 0f32e4596f1c5..4c3da00ed7cad 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -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; 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([]); const [isLoading, setIsLoading] = useState(false); const [items, setItems] = useState([]); const [selectedModels, setSelectedModels] = useState([]); @@ -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) => {type}, + render: (types: string[]) => ( + + {types.map((type) => ( + + {type} + + ))} + + ), }, { field: ModelsTableToConfigMapping.createdAt, @@ -459,12 +463,6 @@ export const ModelsList: FC = () => { ] : []; - const { onTableChange, pagination, sorting } = useTableSettings( - filteredModels, - pageState, - updatePageState - ); - const toolsLeft = ( @@ -496,15 +494,27 @@ export const ModelsList: FC = () => { const selection: EuiTableSelectionType | 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 ( <> @@ -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) => ({