mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] ELSER v2 download in the Trained Models UI (#167407)
## Summary
Adds support for ELSER v2 download from the Trained Models UI.
- Marks an appropriate model version for the current cluster
configuration with the recommended flag.
- Updates the state column with better human-readable labels and colour
indicators.
- Adds a callout promoting a new version of ELSER
<img width="1686" alt="image"
src="0deea53a
-6d37-4af6-97bc-9f46e36f113b">
#### Notes for reviews
- We need to wait for
https://github.com/elastic/elasticsearch/pull/99584 to get the start
deployment validation functionality. At the moment you can successfully
start deployment of the wrong model version.
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
105935ace5
commit
0c6dfbf209
10 changed files with 375 additions and 77 deletions
|
@ -510,6 +510,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`,
|
||||
trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`,
|
||||
startTrainedModelsDeployment: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-model.html`,
|
||||
nlpElser: `${MACHINE_LEARNING_DOCS}ml-nlp-elser.html`,
|
||||
},
|
||||
transforms: {
|
||||
guide: `${ELASTICSEARCH_DOCS}transforms.html`,
|
||||
|
|
|
@ -20,4 +20,9 @@ export {
|
|||
type ModelDefinitionResponse,
|
||||
type ElserVersion,
|
||||
type GetElserOptions,
|
||||
ELSER_ID_V1,
|
||||
ELASTIC_MODEL_TAG,
|
||||
ELASTIC_MODEL_TYPE,
|
||||
MODEL_STATE,
|
||||
type ModelState,
|
||||
} from './src/constants/trained_models';
|
||||
|
|
|
@ -46,8 +46,12 @@ export const BUILT_IN_MODEL_TAG = 'prepackaged';
|
|||
|
||||
export const ELASTIC_MODEL_TAG = 'elastic';
|
||||
|
||||
export const ELSER_ID_V1 = '.elser_model_1' as const;
|
||||
|
||||
export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object.freeze({
|
||||
'.elser_model_1': {
|
||||
modelName: 'elser',
|
||||
hidden: true,
|
||||
version: 1,
|
||||
config: {
|
||||
input: {
|
||||
|
@ -59,6 +63,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
|
|||
}),
|
||||
},
|
||||
'.elser_model_2_SNAPSHOT': {
|
||||
modelName: 'elser',
|
||||
version: 2,
|
||||
default: true,
|
||||
config: {
|
||||
|
@ -71,6 +76,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
|
|||
}),
|
||||
},
|
||||
'.elser_model_2_linux-x86_64_SNAPSHOT': {
|
||||
modelName: 'elser',
|
||||
version: 2,
|
||||
os: 'Linux',
|
||||
arch: 'amd64',
|
||||
|
@ -87,6 +93,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
|
|||
} as const);
|
||||
|
||||
export interface ModelDefinition {
|
||||
modelName: string;
|
||||
version: number;
|
||||
config: object;
|
||||
description: string;
|
||||
|
@ -94,9 +101,10 @@ export interface ModelDefinition {
|
|||
arch?: string;
|
||||
default?: boolean;
|
||||
recommended?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export type ModelDefinitionResponse = ModelDefinition & {
|
||||
export type ModelDefinitionResponse = Omit<ModelDefinition, 'modelName'> & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
@ -106,6 +114,7 @@ export const MODEL_STATE = {
|
|||
...DEPLOYMENT_STATE,
|
||||
DOWNLOADING: 'downloading',
|
||||
DOWNLOADED: 'downloaded',
|
||||
NOT_DOWNLOADED: 'notDownloaded',
|
||||
} as const;
|
||||
|
||||
export type ModelState = typeof MODEL_STATE[keyof typeof MODEL_STATE] | null;
|
||||
|
|
|
@ -15,6 +15,7 @@ export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
|
|||
export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels';
|
||||
export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt';
|
||||
export const ML_OVERVIEW_PANELS = 'ml.overviewPanels';
|
||||
export const ML_ELSER_CALLOUT_DISMISSED = 'ml.elserUpdateCalloutDismissed';
|
||||
|
||||
export type PartitionFieldConfig =
|
||||
| {
|
||||
|
@ -68,6 +69,7 @@ export interface MlStorageRecord {
|
|||
[ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined;
|
||||
[ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined;
|
||||
[ML_OVERVIEW_PANELS]: OverviewPanelsState;
|
||||
[ML_ELSER_CALLOUT_DISMISSED]: boolean | undefined;
|
||||
}
|
||||
|
||||
export type MlStorage = Partial<MlStorageRecord> | null;
|
||||
|
@ -88,6 +90,8 @@ export type TMlStorageMapped<T extends MlStorageKey> = T extends typeof ML_ENTIT
|
|||
? number | undefined
|
||||
: T extends typeof ML_OVERVIEW_PANELS
|
||||
? OverviewPanelsState | undefined
|
||||
: T extends typeof ML_ELSER_CALLOUT_DISMISSED
|
||||
? boolean | undefined
|
||||
: null;
|
||||
|
||||
export const ML_STORAGE_KEYS = [
|
||||
|
@ -98,4 +102,5 @@ export const ML_STORAGE_KEYS = [
|
|||
ML_ANOMALY_EXPLORER_PANELS,
|
||||
ML_NOTIFICATIONS_LAST_CHECKED_AT,
|
||||
ML_OVERVIEW_PANELS,
|
||||
ML_ELSER_CALLOUT_DISMISSED,
|
||||
] as const;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MODEL_STATE, ModelState } from '@kbn/ml-trained-models-utils';
|
||||
import { EuiHealthProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const getModelStateColor = (
|
||||
state: ModelState
|
||||
): { color: EuiHealthProps['color']; name: string } | null => {
|
||||
switch (state) {
|
||||
case MODEL_STATE.DOWNLOADED:
|
||||
return {
|
||||
color: 'subdued',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadedName', {
|
||||
defaultMessage: 'Ready to deploy',
|
||||
}),
|
||||
};
|
||||
case MODEL_STATE.DOWNLOADING:
|
||||
return {
|
||||
color: 'warning',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadingName', {
|
||||
defaultMessage: 'Downloading...',
|
||||
}),
|
||||
};
|
||||
case MODEL_STATE.STARTED:
|
||||
return {
|
||||
color: 'success',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startedName', {
|
||||
defaultMessage: 'Deployed',
|
||||
}),
|
||||
};
|
||||
case MODEL_STATE.STARTING:
|
||||
return {
|
||||
color: 'success',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startingName', {
|
||||
defaultMessage: 'Starting deployment...',
|
||||
}),
|
||||
};
|
||||
case MODEL_STATE.STOPPING:
|
||||
return {
|
||||
color: 'accent',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.stoppingName', {
|
||||
defaultMessage: 'Stopping deployment...',
|
||||
}),
|
||||
};
|
||||
case MODEL_STATE.NOT_DOWNLOADED:
|
||||
return {
|
||||
color: '#d4dae5',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.notDownloadedName', {
|
||||
defaultMessage: 'Not downloaded',
|
||||
}),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -177,17 +177,18 @@ export function useModelActions({
|
|||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.inference.modelsList.startModelDeploymentActionLabel', {
|
||||
defaultMessage: 'Start deployment',
|
||||
defaultMessage: 'Deploy',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.ml.inference.modelsList.startModelDeploymentActionLabel',
|
||||
'xpack.ml.inference.modelsList.startModelDeploymentActionDescription',
|
||||
{
|
||||
defaultMessage: 'Start deployment',
|
||||
}
|
||||
),
|
||||
'data-test-subj': 'mlModelsTableRowStartDeploymentAction',
|
||||
// @ts-ignore EUI has a type check issue when type "button" is combined with an icon.
|
||||
icon: 'play',
|
||||
type: 'icon',
|
||||
type: 'button',
|
||||
isPrimary: true,
|
||||
enabled: (item) => {
|
||||
return canStartStopTrainedModels && !isLoading && item.state !== MODEL_STATE.DOWNLOADING;
|
||||
|
@ -311,10 +312,12 @@ export function useModelActions({
|
|||
'data-test-subj': 'mlModelsTableRowStopDeploymentAction',
|
||||
icon: 'stop',
|
||||
type: 'icon',
|
||||
isPrimary: true,
|
||||
available: (item) => item.model_type === TRAINED_MODEL_TYPE.PYTORCH,
|
||||
enabled: (item) =>
|
||||
canStartStopTrainedModels && !isLoading && item.deployment_ids.length > 0,
|
||||
isPrimary: false,
|
||||
available: (item) =>
|
||||
item.model_type === TRAINED_MODEL_TYPE.PYTORCH &&
|
||||
canStartStopTrainedModels &&
|
||||
(item.state === MODEL_STATE.STARTED || item.state === MODEL_STATE.STARTING),
|
||||
enabled: (item) => !isLoading,
|
||||
onClick: async (item) => {
|
||||
const requireForceStop = isPopulatedObject(item.pipelines);
|
||||
const hasMultipleDeployments = item.deployment_ids.length > 1;
|
||||
|
@ -380,17 +383,19 @@ export function useModelActions({
|
|||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', {
|
||||
defaultMessage: 'Download model',
|
||||
defaultMessage: 'Download',
|
||||
}),
|
||||
description: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', {
|
||||
defaultMessage: 'Download model',
|
||||
defaultMessage: 'Download',
|
||||
}),
|
||||
'data-test-subj': 'mlModelsTableRowDownloadModelAction',
|
||||
// @ts-ignore EUI has a type check issue when type "button" is combined with an icon.
|
||||
icon: 'download',
|
||||
type: 'icon',
|
||||
type: 'button',
|
||||
isPrimary: true,
|
||||
available: (item) => item.tags.includes(ELASTIC_MODEL_TAG),
|
||||
enabled: (item) => !item.state && !isLoading,
|
||||
available: (item) =>
|
||||
item.tags.includes(ELASTIC_MODEL_TAG) && item.state === MODEL_STATE.NOT_DOWNLOADED,
|
||||
enabled: (item) => !isLoading,
|
||||
onClick: async (item) => {
|
||||
try {
|
||||
onLoading(true);
|
||||
|
|
|
@ -10,12 +10,16 @@ import {
|
|||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiInMemoryTable,
|
||||
EuiSearchBarProps,
|
||||
EuiLink,
|
||||
type EuiSearchBarProps,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
SearchFilterConfig,
|
||||
} from '@elastic/eui';
|
||||
import { groupBy } from 'lodash';
|
||||
|
@ -28,18 +32,21 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
|||
import { usePageUrlState } from '@kbn/ml-url-state';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import {
|
||||
BUILT_IN_MODEL_TYPE,
|
||||
BUILT_IN_MODEL_TAG,
|
||||
BUILT_IN_MODEL_TYPE,
|
||||
DEPLOYMENT_STATE,
|
||||
} from '@kbn/ml-trained-models-utils';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import {
|
||||
ELASTIC_MODEL_DEFINITIONS,
|
||||
ELASTIC_MODEL_TAG,
|
||||
ELASTIC_MODEL_TYPE,
|
||||
ELSER_ID_V1,
|
||||
MODEL_STATE,
|
||||
ModelState,
|
||||
} from '@kbn/ml-trained-models-utils/src/constants/trained_models';
|
||||
type ModelState,
|
||||
} from '@kbn/ml-trained-models-utils';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { css } from '@emotion/react';
|
||||
import { useStorage } from '@kbn/ml-local-storage';
|
||||
import { getModelStateColor } from './get_model_state_color';
|
||||
import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage';
|
||||
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
|
||||
import { useModelActions } from './model_actions';
|
||||
import { ModelsTableToConfigMapping } from '.';
|
||||
|
@ -74,6 +81,7 @@ export type ModelItem = TrainedModelConfigResponse & {
|
|||
deployment_ids: string[];
|
||||
putModelConfig?: object;
|
||||
state: ModelState;
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
export type ModelItemFull = Required<ModelItem>;
|
||||
|
@ -83,10 +91,14 @@ interface PageUrlState {
|
|||
pageUrlState: ListingPageUrlState;
|
||||
}
|
||||
|
||||
const modelIdColumnName = i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', {
|
||||
defaultMessage: 'ID',
|
||||
});
|
||||
|
||||
export const getDefaultModelsListState = (): ListingPageUrlState => ({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
sortField: ModelsTableToConfigMapping.id,
|
||||
sortField: modelIdColumnName,
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
|
||||
|
@ -102,10 +114,17 @@ export const ModelsList: FC<Props> = ({
|
|||
const {
|
||||
services: {
|
||||
application: { capabilities },
|
||||
docLinks,
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const nlpElserDocUrl = docLinks.links.ml.nlpElser;
|
||||
|
||||
const { isNLPEnabled } = useEnabledFeatures();
|
||||
const [isElserCalloutDismissed, setIsElserCalloutDismissed] = useStorage(
|
||||
ML_ELSER_CALLOUT_DISMISSED,
|
||||
false
|
||||
);
|
||||
|
||||
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
|
||||
|
||||
|
@ -155,6 +174,11 @@ export const ModelsList: FC<Props> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
// List of downloaded/existing models
|
||||
const existingModels = useMemo(() => {
|
||||
return items.filter((i) => !i.putModelConfig);
|
||||
}, [items]);
|
||||
|
||||
/**
|
||||
* Checks if the model download complete.
|
||||
*/
|
||||
|
@ -219,7 +243,35 @@ export const ModelsList: FC<Props> = ({
|
|||
// TODO combine fetching models definitions and stats into a single function
|
||||
await fetchModelsStats(newItems);
|
||||
|
||||
setItems(newItems);
|
||||
let resultItems = newItems;
|
||||
// don't add any of the built-in models (e.g. elser) if NLP is disabled
|
||||
if (isNLPEnabled) {
|
||||
const idMap = new Map<string, ModelItem>(
|
||||
resultItems.map((model) => [model.model_id, model])
|
||||
);
|
||||
const forDownload = await trainedModelsApiService.getTrainedModelDownloads();
|
||||
const notDownloaded: ModelItem[] = forDownload
|
||||
.filter(({ name, hidden, recommended }) => {
|
||||
if (recommended && idMap.has(name)) {
|
||||
idMap.get(name)!.recommended = true;
|
||||
}
|
||||
return !idMap.has(name) && !hidden;
|
||||
})
|
||||
.map<ModelItem>((modelDefinition) => {
|
||||
return {
|
||||
model_id: modelDefinition.name,
|
||||
type: [ELASTIC_MODEL_TYPE],
|
||||
tags: [ELASTIC_MODEL_TAG],
|
||||
putModelConfig: modelDefinition.config,
|
||||
description: modelDefinition.description,
|
||||
state: MODEL_STATE.NOT_DOWNLOADED,
|
||||
recommended: !!modelDefinition.recommended,
|
||||
} as ModelItem;
|
||||
});
|
||||
resultItems = [...resultItems, ...notDownloaded];
|
||||
}
|
||||
|
||||
setItems(resultItems);
|
||||
|
||||
if (expandedItemsToRefresh.length > 0) {
|
||||
await fetchModelsStats(expandedItemsToRefresh);
|
||||
|
@ -242,7 +294,7 @@ export const ModelsList: FC<Props> = ({
|
|||
setIsInitialized(true);
|
||||
setIsLoading(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemIdToExpandedRowMap]);
|
||||
}, [itemIdToExpandedRowMap, isNLPEnabled]);
|
||||
|
||||
useEffect(
|
||||
function updateOnTimerRefresh() {
|
||||
|
@ -257,13 +309,13 @@ export const ModelsList: FC<Props> = ({
|
|||
return {
|
||||
total: {
|
||||
show: true,
|
||||
value: items.length,
|
||||
value: existingModels.length,
|
||||
label: i18n.translate('xpack.ml.trainedModels.modelsList.totalAmountLabel', {
|
||||
defaultMessage: 'Total trained models',
|
||||
}),
|
||||
},
|
||||
};
|
||||
}, [items]);
|
||||
}, [existingModels]);
|
||||
|
||||
/**
|
||||
* Fetches models stats and update the original object
|
||||
|
@ -325,7 +377,7 @@ export const ModelsList: FC<Props> = ({
|
|||
* Unique inference types from models
|
||||
*/
|
||||
const inferenceTypesOptions = useMemo(() => {
|
||||
const result = items.reduce((acc, item) => {
|
||||
const result = existingModels.reduce((acc, item) => {
|
||||
const type = item.inference_config && Object.keys(item.inference_config)[0];
|
||||
if (type) {
|
||||
acc.add(type);
|
||||
|
@ -339,13 +391,16 @@ export const ModelsList: FC<Props> = ({
|
|||
value: v,
|
||||
name: v,
|
||||
}));
|
||||
}, [items]);
|
||||
}, [existingModels]);
|
||||
|
||||
const modelAndDeploymentIds = useMemo(
|
||||
() => [
|
||||
...new Set([...items.flatMap((v) => v.deployment_ids), ...items.map((i) => i.model_id)]),
|
||||
...new Set([
|
||||
...existingModels.flatMap((v) => v.deployment_ids),
|
||||
...existingModels.map((i) => i.model_id),
|
||||
]),
|
||||
],
|
||||
[items]
|
||||
[existingModels]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -400,30 +455,61 @@ export const ModelsList: FC<Props> = ({
|
|||
'data-test-subj': 'mlModelsTableRowDetailsToggle',
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.id,
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
sortable: true,
|
||||
name: modelIdColumnName,
|
||||
width: '15%',
|
||||
sortable: ({ model_id: modelId }: ModelItem) => modelId,
|
||||
truncateText: false,
|
||||
textOnly: false,
|
||||
'data-test-subj': 'mlModelsTableColumnId',
|
||||
render: ({ description, model_id: modelId }: ModelItem) => {
|
||||
const isTechPreview = description?.includes('(Tech Preview)');
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems={'center'} wrap={true}>
|
||||
<EuiFlexItem grow={false}>{modelId}</EuiFlexItem>
|
||||
{isTechPreview ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge compressed />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.description,
|
||||
width: '350px',
|
||||
width: '35%',
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelDescriptionHeader', {
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
'data-test-subj': 'mlModelsTableColumnDescription',
|
||||
render: (description: string) => {
|
||||
render: ({ description, recommended }: ModelItem) => {
|
||||
if (!description) return null;
|
||||
const isTechPreview = description.includes('(Tech Preview)');
|
||||
return (
|
||||
<>
|
||||
{description.replace('(Tech Preview)', '')}
|
||||
{isTechPreview ? <TechnicalPreviewBadge compressed /> : null}
|
||||
{recommended ? (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.recommendedDownloadContent"
|
||||
defaultMessage="Recommended ELSER model version for your cluster's hardware configuration"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<b
|
||||
css={css`
|
||||
text-wrap: nowrap;
|
||||
`}
|
||||
>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.recommendedDownloadLabel"
|
||||
defaultMessage="(Recommended)"
|
||||
/>
|
||||
</b>
|
||||
</EuiToolTip>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
@ -456,8 +542,13 @@ export const ModelsList: FC<Props> = ({
|
|||
}),
|
||||
align: 'left',
|
||||
truncateText: false,
|
||||
render: (state: string) => {
|
||||
return state ? <EuiBadge color="hollow">{state}</EuiBadge> : null;
|
||||
render: (state: ModelState) => {
|
||||
const config = getModelStateColor(state);
|
||||
return config ? (
|
||||
<EuiHealth textSize={'xs'} color={config.color}>
|
||||
{config.name}
|
||||
</EuiHealth>
|
||||
) : null;
|
||||
},
|
||||
'data-test-subj': 'mlModelsTableColumnDeploymentState',
|
||||
},
|
||||
|
@ -585,28 +676,8 @@ export const ModelsList: FC<Props> = ({
|
|||
: {}),
|
||||
};
|
||||
|
||||
const resultItems = useMemo<ModelItem[]>(() => {
|
||||
if (isNLPEnabled === false) {
|
||||
// don't add any of the built in models (e.g. elser) if NLP is disabled
|
||||
return items;
|
||||
}
|
||||
|
||||
const idSet = new Set(items.map((i) => i.model_id));
|
||||
const notDownloaded: ModelItem[] = Object.entries(ELASTIC_MODEL_DEFINITIONS)
|
||||
.filter(([modelId]) => !idSet.has(modelId))
|
||||
.map(([modelId, modelDefinition]) => {
|
||||
return {
|
||||
model_id: modelId,
|
||||
type: [ELASTIC_MODEL_TYPE],
|
||||
tags: [ELASTIC_MODEL_TAG],
|
||||
putModelConfig: modelDefinition.config,
|
||||
description: modelDefinition.description,
|
||||
} as ModelItem;
|
||||
});
|
||||
const result = [...items, ...notDownloaded];
|
||||
|
||||
return result;
|
||||
}, [isNLPEnabled, items]);
|
||||
const isElserCalloutVisible =
|
||||
!isElserCalloutDismissed && items.findIndex((i) => i.model_id === ELSER_ID_V1) >= 0;
|
||||
|
||||
if (!isInitialized) return null;
|
||||
|
||||
|
@ -631,7 +702,7 @@ export const ModelsList: FC<Props> = ({
|
|||
isExpandable={true}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isSelectable={false}
|
||||
items={resultItems}
|
||||
items={items}
|
||||
itemId={ModelsTableToConfigMapping.id}
|
||||
loading={isLoading}
|
||||
search={search}
|
||||
|
@ -643,6 +714,38 @@ export const ModelsList: FC<Props> = ({
|
|||
onTableChange={onTableChange}
|
||||
sorting={sorting}
|
||||
data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
|
||||
childrenBetween={
|
||||
isElserCalloutVisible ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.newElserModelTitle"
|
||||
defaultMessage="New ELSER model now available"
|
||||
/>
|
||||
}
|
||||
onDismiss={setIsElserCalloutDismissed.bind(null, true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.newElserModelDescription"
|
||||
defaultMessage="A new version of ELSER that shows faster performance and improved relevance is now available. {docLink} for information on how to start using it."
|
||||
values={{
|
||||
docLink: (
|
||||
<EuiLink href={nlpElserDocUrl} external target={'_blank'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink"
|
||||
defaultMessage="View documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{modelsToDelete.length > 0 && (
|
||||
|
|
|
@ -43,6 +43,89 @@ describe('modelsProvider', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getModelDownloads', () => {
|
||||
test('provides a list of models with recommended and default flag', async () => {
|
||||
const result = await modelService.getModelDownloads();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)',
|
||||
hidden: true,
|
||||
name: '.elser_model_1',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
default: true,
|
||||
description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)',
|
||||
name: '.elser_model_2_SNAPSHOT',
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
arch: 'amd64',
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
description:
|
||||
'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)',
|
||||
name: '.elser_model_2_linux-x86_64_SNAPSHOT',
|
||||
os: 'Linux',
|
||||
recommended: true,
|
||||
version: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('provides a list of models with default model as recommended', async () => {
|
||||
mockCloud.cloudId = undefined;
|
||||
(mockClient.asInternalUser.transport.request as jest.Mock).mockResolvedValueOnce({
|
||||
_nodes: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
},
|
||||
cluster_name: 'default',
|
||||
nodes: {
|
||||
yYmqBqjpQG2rXsmMSPb9pQ: {
|
||||
name: 'node-0',
|
||||
roles: ['ml'],
|
||||
attributes: {},
|
||||
os: {
|
||||
name: 'Mac OS X',
|
||||
arch: 'aarch64',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await modelService.getModelDownloads();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)',
|
||||
hidden: true,
|
||||
name: '.elser_model_1',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
recommended: true,
|
||||
description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)',
|
||||
name: '.elser_model_2_SNAPSHOT',
|
||||
version: 2,
|
||||
},
|
||||
{
|
||||
arch: 'amd64',
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
description:
|
||||
'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)',
|
||||
name: '.elser_model_2_linux-x86_64_SNAPSHOT',
|
||||
os: 'Linux',
|
||||
version: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getELSER', () => {
|
||||
test('provides a recommended definition by default', async () => {
|
||||
const result = await modelService.getELSER();
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
|||
import type { PipelineDefinition } from '../../../common/types/trained_models';
|
||||
|
||||
export type ModelService = ReturnType<typeof modelsProvider>;
|
||||
|
||||
export const modelsProvider = (client: IScopedClusterClient, cloud?: CloudSetup) =>
|
||||
new ModelsProvider(client, cloud);
|
||||
|
||||
|
@ -83,6 +84,7 @@ export class ModelsProvider {
|
|||
return { [index]: null };
|
||||
}
|
||||
}
|
||||
|
||||
private getNodeId(
|
||||
elementOriginalId: string,
|
||||
nodeType: typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]
|
||||
|
@ -446,18 +448,39 @@ export class ModelsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
const result = Object.entries(ELASTIC_MODEL_DEFINITIONS).map(([name, def]) => {
|
||||
const modelDefinitionMap = new Map<string, ModelDefinitionResponse[]>();
|
||||
|
||||
for (const [name, def] of Object.entries(ELASTIC_MODEL_DEFINITIONS)) {
|
||||
const recommended =
|
||||
(isCloud && def.os === 'Linux' && def.arch === 'amd64') ||
|
||||
(sameArch && !!def?.os && def?.os === osName && def?.arch === arch);
|
||||
return {
|
||||
...def,
|
||||
name,
|
||||
...(recommended ? { recommended } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
const { modelName, ...rest } = def;
|
||||
|
||||
const modelDefinitionResponse = {
|
||||
...rest,
|
||||
...(recommended ? { recommended } : {}),
|
||||
name,
|
||||
};
|
||||
|
||||
if (modelDefinitionMap.has(modelName)) {
|
||||
modelDefinitionMap.get(modelName)!.push(modelDefinitionResponse);
|
||||
} else {
|
||||
modelDefinitionMap.set(modelName, [modelDefinitionResponse]);
|
||||
}
|
||||
}
|
||||
|
||||
// check if there is no recommended, so we mark default as recommended
|
||||
for (const arr of modelDefinitionMap.values()) {
|
||||
const defaultModel = arr.find((a) => a.default);
|
||||
const recommendedModel = arr.find((a) => a.recommended);
|
||||
if (defaultModel && !recommendedModel) {
|
||||
delete defaultModel.default;
|
||||
defaultModel.recommended = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [...modelDefinitionMap.values()].flat();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -264,9 +264,11 @@ export function TrainedModelsTableProvider(
|
|||
}
|
||||
|
||||
public async deleteModel(modelId: string) {
|
||||
const fromContextMenu = await this.doesModelCollapsedActionsButtonExist(modelId);
|
||||
await mlCommonUI.invokeTableRowAction(
|
||||
this.rowSelector(modelId),
|
||||
'mlModelsTableRowDeleteAction'
|
||||
'mlModelsTableRowDeleteAction',
|
||||
fromContextMenu
|
||||
);
|
||||
await this.assertDeleteModalExists();
|
||||
await this.confirmDeleteModel();
|
||||
|
@ -459,9 +461,10 @@ export function TrainedModelsTableProvider(
|
|||
}
|
||||
|
||||
public async clickStopDeploymentAction(modelId: string) {
|
||||
await testSubjects.clickWhenNotDisabled(
|
||||
this.rowSelector(modelId, 'mlModelsTableRowStopDeploymentAction'),
|
||||
{ timeout: 5000 }
|
||||
await mlCommonUI.invokeTableRowAction(
|
||||
this.rowSelector(modelId),
|
||||
'mlModelsTableRowStopDeploymentAction',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue