mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] "Add model" flyout for the Trained Models UI (#171024)
## Summary Adds the "Add trained model" button to the Trained Models UI that opens the flyout with available models for download. It also contains the "Third-party" tab with instructions for deploying 3rd party models with Eland. <img width="1685" alt="image" src="73cf81ae
-b761-4808-a89d-e70235a9fd2f"> <img width="1337" alt="image" src="33984952
-32c4-4ab1-9160-6f585b1d7968"> <img width="1685" alt="image" src="0060a7dd
-9875-4884-a83a-4e277d53942b"> ### 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 - [ ] [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=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [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
8af39c3836
commit
c8537bf964
8 changed files with 541 additions and 36 deletions
|
@ -518,6 +518,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
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`,
|
||||
nlpImportModel: `${MACHINE_LEARNING_DOCS}ml-nlp-import-model.html`,
|
||||
},
|
||||
transforms: {
|
||||
guide: `${ELASTICSEARCH_DOCS}transforms.html`,
|
||||
|
@ -813,6 +814,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`,
|
||||
rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`,
|
||||
rustOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/overview.html`,
|
||||
eland: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/eland/${DOC_LINK_VERSION}/index.html`,
|
||||
},
|
||||
endpoints: {
|
||||
troubleshooting: `${SECURITY_SOLUTION_DOCS}ts-management.html#ts-endpoints`,
|
||||
|
|
|
@ -570,6 +570,7 @@ export interface DocLinks {
|
|||
readonly rubyOverview: string;
|
||||
readonly rustGuide: string;
|
||||
readonly rustOverview: string;
|
||||
readonly eland: string;
|
||||
};
|
||||
readonly endpoints: {
|
||||
readonly troubleshooting: string;
|
||||
|
|
|
@ -92,8 +92,14 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
|
|||
} as const);
|
||||
|
||||
export interface ModelDefinition {
|
||||
/**
|
||||
* Model name, e.g. elser
|
||||
*/
|
||||
modelName: string;
|
||||
version: number;
|
||||
/**
|
||||
* Default PUT model configuration
|
||||
*/
|
||||
config: object;
|
||||
description: string;
|
||||
os?: string;
|
||||
|
@ -103,7 +109,10 @@ export interface ModelDefinition {
|
|||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export type ModelDefinitionResponse = Omit<ModelDefinition, 'modelName'> & {
|
||||
export type ModelDefinitionResponse = ModelDefinition & {
|
||||
/**
|
||||
* Complete model id, e.g. .elser_model_2_linux-x86_64
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,448 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCheckableCard,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormFieldset,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiSteps,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { type FC, useMemo, useState } from 'react';
|
||||
import { groupBy } from 'lodash';
|
||||
import { usePermissionCheck } from '../capabilities/check_capabilities';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { ModelItem } from './models_list';
|
||||
|
||||
export interface AddModelFlyoutProps {
|
||||
modelDownloads: ModelItem[];
|
||||
onClose: () => void;
|
||||
onSubmit: (modelId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flyout for downloading elastic curated models and showing instructions for importing third-party models.
|
||||
*/
|
||||
export const AddModelFlyout: FC<AddModelFlyoutProps> = ({ onClose, onSubmit, modelDownloads }) => {
|
||||
const canCreateTrainedModels = usePermissionCheck('canCreateTrainedModels');
|
||||
const isElserTabVisible = canCreateTrainedModels && modelDownloads.length > 0;
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(isElserTabVisible ? 'elser' : 'thirdParty');
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
...(isElserTabVisible
|
||||
? [
|
||||
{
|
||||
id: 'elser',
|
||||
name: (
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="logoElastic" size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.elserTabLabel"
|
||||
defaultMessage="ELSER"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
content: (
|
||||
<ElserTabContent modelDownloads={modelDownloads} onModelDownload={onSubmit} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'thirdParty',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdPartyLabel"
|
||||
defaultMessage="Third-party"
|
||||
/>
|
||||
),
|
||||
content: <ThirdPartyTabContent />,
|
||||
},
|
||||
];
|
||||
}, [isElserTabVisible, modelDownloads, onSubmit]);
|
||||
|
||||
const selectedTabContent = useMemo(() => {
|
||||
return tabs.find((obj) => obj.id === selectedTabId)?.content;
|
||||
}, [selectedTabId, tabs]);
|
||||
|
||||
return (
|
||||
<EuiFlyout ownFocus onClose={onClose} aria-labelledby={'addTrainedModelFlyout'}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id={'addTrainedModelFlyout'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.title"
|
||||
defaultMessage="Add a trained model"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiTabs>
|
||||
{tabs.map((tab) => (
|
||||
<EuiTab
|
||||
key={tab.id}
|
||||
isSelected={selectedTabId === tab.id}
|
||||
onClick={setSelectedTabId.bind(null, tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
interface ElserTabContentProps {
|
||||
modelDownloads: ModelItem[];
|
||||
onModelDownload: (modelId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ELSER tab content for selecting a model to download.
|
||||
*/
|
||||
const ElserTabContent: FC<ElserTabContentProps> = ({ modelDownloads, onModelDownload }) => {
|
||||
const {
|
||||
services: { docLinks },
|
||||
} = useMlKibana();
|
||||
|
||||
const [selectedModelId, setSelectedModelId] = useState<string | undefined>(
|
||||
modelDownloads.find((m) => m.recommended)?.model_id
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(groupBy(modelDownloads, 'modelName')).map(([modelName, models]) => {
|
||||
return (
|
||||
<React.Fragment key={modelName}>
|
||||
{modelName === 'elser' ? (
|
||||
<div>
|
||||
<EuiTitle size={'s'}>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.elserTitle"
|
||||
defaultMessage="Elastic Learned Sparse EncodeR (ELSER)"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<EuiText color={'subdued'} size={'s'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.elserDescription"
|
||||
defaultMessage="ELSER is designed to efficiently use context in natural language queries with better results than BM25 alone."
|
||||
/>
|
||||
</EuiText>
|
||||
</p>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<EuiLink href={docLinks.links.ml.nlpElser} external>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.elserViewDocumentationLinkLabel"
|
||||
defaultMessage="View documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
<EuiSpacer size={'m'} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<EuiFormFieldset
|
||||
legend={{
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.chooseModelLabel"
|
||||
defaultMessage="Choose a model"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{models.map((model) => {
|
||||
return (
|
||||
<React.Fragment key={model.model_id}>
|
||||
<EuiCheckableCard
|
||||
id={model.model_id}
|
||||
label={
|
||||
<EuiFlexGroup
|
||||
gutterSize={'s'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'spaceBetween'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<header>
|
||||
<EuiText size={'s'}>
|
||||
<b>
|
||||
{model.os === 'Linux' && model.arch === 'amd64' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.intelLinuxLabel"
|
||||
defaultMessage="Intel and Linux optimized"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.crossPlatformLabel"
|
||||
defaultMessage="Cross platform"
|
||||
/>
|
||||
)}
|
||||
</b>
|
||||
</EuiText>
|
||||
</header>
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
{model.model_id}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{model.recommended ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.recommendedDownloadContent"
|
||||
defaultMessage="Recommended ELSER model version for your cluster's hardware configuration"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.recommendedDownloadLabel"
|
||||
defaultMessage="Recommended"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
name={model.model_id}
|
||||
value={model.model_id}
|
||||
checked={model.model_id === selectedModelId}
|
||||
onChange={setSelectedModelId.bind(null, model.model_id)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</EuiFormFieldset>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<EuiButton
|
||||
onClick={onModelDownload.bind(null, selectedModelId!)}
|
||||
fill
|
||||
disabled={!selectedModelId}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.downloadButtonLabel"
|
||||
defaultMessage="Download"
|
||||
/>
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Third-party tab content for showing instructions for importing third-party models.
|
||||
*/
|
||||
const ThirdPartyTabContent: FC = () => {
|
||||
const {
|
||||
services: { docLinks },
|
||||
} = useMlKibana();
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiSteps
|
||||
steps={[
|
||||
{
|
||||
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step1Title', {
|
||||
defaultMessage: 'Install the Eland Python Client',
|
||||
}),
|
||||
children: (
|
||||
<EuiText>
|
||||
<p>
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.pipInstallLabel"
|
||||
defaultMessage="Eland can be installed with {pipLink} from {pypiLink}:"
|
||||
values={{
|
||||
pipLink: (
|
||||
<EuiLink
|
||||
href={'https://pypi.org/project/pip/'}
|
||||
target={'_blank'}
|
||||
external
|
||||
>
|
||||
pip
|
||||
</EuiLink>
|
||||
),
|
||||
pypiLink: (
|
||||
<EuiLink href={'https://pypi.org/'} target={'_blank'} external>
|
||||
PyPI
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</p>
|
||||
<p>
|
||||
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
|
||||
$ python -m pip install eland
|
||||
</EuiCodeBlock>
|
||||
</p>
|
||||
<p>
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.condaInstallLabel"
|
||||
defaultMessage="or it can also be installed with {condaLink} from {condaForgeLink}:"
|
||||
values={{
|
||||
condaLink: (
|
||||
<EuiLink href={'https://docs.conda.io/'} target={'_blank'} external>
|
||||
Conda
|
||||
</EuiLink>
|
||||
),
|
||||
condaForgeLink: (
|
||||
<EuiLink href={'https://conda-forge.org/'} target={'_blank'} external>
|
||||
Conda Forge
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</p>
|
||||
<p>
|
||||
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
|
||||
$ conda install -c conda-forge eland
|
||||
</EuiCodeBlock>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step2Title', {
|
||||
defaultMessage: 'Importing your third-party model',
|
||||
}),
|
||||
children: (
|
||||
<EuiText>
|
||||
<p>
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step2Body"
|
||||
defaultMessage="Follow the instructions on importing compatible third-party models"
|
||||
/>
|
||||
</EuiText>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step2ExampleTitle"
|
||||
defaultMessage="Example import"
|
||||
/>
|
||||
</b>
|
||||
|
||||
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
|
||||
eland_import_hub_model <br />
|
||||
--cloud-id <cloud-id> \ <br />
|
||||
-u <username> -p <password> \ <br />
|
||||
--hub-model-id <model-id> \ <br />
|
||||
--task-type ner \
|
||||
</EuiCodeBlock>
|
||||
</p>
|
||||
|
||||
<EuiFlexGroup gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={docLinks.links.ml.nlpImportModel}
|
||||
target={'_blank'}
|
||||
iconType={'help'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.importModelButtonLabel"
|
||||
defaultMessage="Import models with Eland"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={docLinks.links.enterpriseSearch.supportedNlpModels}
|
||||
target={'_blank'}
|
||||
iconType={'help'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.compatibleModelsButtonLabel"
|
||||
defaultMessage="Compatible NLP models"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step4Title', {
|
||||
defaultMessage: 'Deploy your model',
|
||||
}),
|
||||
children: (
|
||||
<>
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step4Body"
|
||||
defaultMessage="Click “Start deployment” in the table row containing your new model to deploy and use it."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiText size={'s'} color={'subdued'}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step3Body"
|
||||
defaultMessage="Note: The trained model list automatically refreshes with the most current imported models in your cluster. If the list is not updated, click the 'Refresh' button in the top right corner. Otherwise, revisit the instructions above to troubleshoot."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -42,12 +42,14 @@ export function useModelActions({
|
|||
isLoading,
|
||||
fetchModels,
|
||||
modelAndDeploymentIds,
|
||||
onModelDownloadRequest,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
onDfaTestAction: (model: ModelItem) => void;
|
||||
onTestAction: (model: ModelItem) => void;
|
||||
onModelsDeleteRequest: (models: ModelItem[]) => void;
|
||||
onModelDeployRequest: (model: ModelItem) => void;
|
||||
onModelDownloadRequest: (modelId: string) => void;
|
||||
onLoading: (isLoading: boolean) => void;
|
||||
fetchModels: () => Promise<void>;
|
||||
modelAndDeploymentIds: string[];
|
||||
|
@ -410,31 +412,7 @@ export function useModelActions({
|
|||
item.state === MODEL_STATE.NOT_DOWNLOADED,
|
||||
enabled: (item) => !isLoading,
|
||||
onClick: async (item) => {
|
||||
try {
|
||||
onLoading(true);
|
||||
await trainedModelsApiService.installElasticTrainedModelConfig(item.model_id);
|
||||
displaySuccessToast(
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.downloadSuccess', {
|
||||
defaultMessage: '"{modelId}" model download has been started successfully.',
|
||||
values: {
|
||||
modelId: item.model_id,
|
||||
},
|
||||
})
|
||||
);
|
||||
// Need to fetch model state updates
|
||||
await fetchModels();
|
||||
} catch (e) {
|
||||
displayErrorToast(
|
||||
e,
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.downloadFailed', {
|
||||
defaultMessage: 'Failed to download "{modelId}"',
|
||||
values: {
|
||||
modelId: item.model_id,
|
||||
},
|
||||
})
|
||||
);
|
||||
onLoading(false);
|
||||
}
|
||||
onModelDownloadRequest(item.model_id);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -614,6 +592,7 @@ export function useModelActions({
|
|||
onTestAction,
|
||||
trainedModelsApiService,
|
||||
urlLocator,
|
||||
onModelDownloadRequest,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
} from '@kbn/ml-trained-models-utils';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { useStorage } from '@kbn/ml-local-storage';
|
||||
import { AddModelFlyout } from './add_model_flyout';
|
||||
import { getModelStateColor } from './get_model_state_color';
|
||||
import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage';
|
||||
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
|
||||
|
@ -84,6 +85,12 @@ export type ModelItem = TrainedModelConfigResponse & {
|
|||
putModelConfig?: object;
|
||||
state: ModelState;
|
||||
recommended?: boolean;
|
||||
/**
|
||||
* Model name, e.g. elser
|
||||
*/
|
||||
modelName?: string;
|
||||
os?: string;
|
||||
arch?: string;
|
||||
};
|
||||
|
||||
export type ModelItemFull = Required<ModelItem>;
|
||||
|
@ -153,7 +160,7 @@ export const ModelsList: FC<Props> = ({
|
|||
|
||||
const trainedModelsApiService = useTrainedModelsApiService();
|
||||
|
||||
const { displayErrorToast } = useToastNotificationService();
|
||||
const { displayErrorToast, displaySuccessToast } = useToastNotificationService();
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -166,6 +173,7 @@ export const ModelsList: FC<Props> = ({
|
|||
);
|
||||
const [modelToTest, setModelToTest] = useState<ModelItem | null>(null);
|
||||
const [dfaModelToTest, setDfaModelToTest] = useState<ModelItem | null>(null);
|
||||
const [isAddModelFlyoutVisible, setIsAddModelFlyoutVisible] = useState(false);
|
||||
|
||||
const isBuiltInModel = useCallback(
|
||||
(item: ModelItem) => item.tags.includes(BUILT_IN_MODEL_TAG),
|
||||
|
@ -269,6 +277,9 @@ export const ModelsList: FC<Props> = ({
|
|||
description: modelDefinition.description,
|
||||
state: MODEL_STATE.NOT_DOWNLOADED,
|
||||
recommended: !!modelDefinition.recommended,
|
||||
modelName: modelDefinition.modelName,
|
||||
os: modelDefinition.os,
|
||||
arch: modelDefinition.arch,
|
||||
} as ModelItem;
|
||||
});
|
||||
resultItems = [...resultItems, ...notDownloaded];
|
||||
|
@ -406,6 +417,33 @@ export const ModelsList: FC<Props> = ({
|
|||
[existingModels]
|
||||
);
|
||||
|
||||
const onModelDownloadRequest = useCallback(
|
||||
async (modelId: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await trainedModelsApiService.installElasticTrainedModelConfig(modelId);
|
||||
displaySuccessToast(
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.downloadSuccess', {
|
||||
defaultMessage: '"{modelId}" model download has been started successfully.',
|
||||
values: { modelId },
|
||||
})
|
||||
);
|
||||
// Need to fetch model state updates
|
||||
await fetchModelsData();
|
||||
} catch (e) {
|
||||
displayErrorToast(
|
||||
e,
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.downloadFailed', {
|
||||
defaultMessage: 'Failed to download "{modelId}"',
|
||||
values: { modelId },
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
}
|
||||
},
|
||||
[displayErrorToast, displaySuccessToast, fetchModelsData, trainedModelsApiService]
|
||||
);
|
||||
|
||||
/**
|
||||
* Table actions
|
||||
*/
|
||||
|
@ -418,6 +456,7 @@ export const ModelsList: FC<Props> = ({
|
|||
onModelDeployRequest: setModelToDeploy,
|
||||
onLoading: setIsLoading,
|
||||
modelAndDeploymentIds,
|
||||
onModelDownloadRequest,
|
||||
});
|
||||
|
||||
const toggleDetails = async (item: ModelItem) => {
|
||||
|
@ -689,13 +728,24 @@ export const ModelsList: FC<Props> = ({
|
|||
<>
|
||||
<SavedObjectsWarning onCloseFlyout={fetchModelsData} forceRefresh={isLoading} />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{modelsStats && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
{modelsStats ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType={'plusInCircle'}
|
||||
color={'primary'}
|
||||
onClick={setIsAddModelFlyoutVisible.bind(null, true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.addModelButtonLabel"
|
||||
defaultMessage="Add trained model"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<div data-test-subj="mlModelsTableContainer">
|
||||
|
@ -776,6 +826,16 @@ export const ModelsList: FC<Props> = ({
|
|||
model={modelToDeploy}
|
||||
/>
|
||||
) : null}
|
||||
{isAddModelFlyoutVisible ? (
|
||||
<AddModelFlyout
|
||||
modelDownloads={items.filter((i) => i.state === MODEL_STATE.NOT_DOWNLOADED)}
|
||||
onClose={setIsAddModelFlyoutVisible.bind(null, false)}
|
||||
onSubmit={(modelId) => {
|
||||
onModelDownloadRequest(modelId);
|
||||
setIsAddModelFlyoutVisible(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('modelsProvider', () => {
|
|||
hidden: true,
|
||||
name: '.elser_model_1',
|
||||
version: 1,
|
||||
modelName: 'elser',
|
||||
},
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
|
@ -63,6 +64,7 @@ describe('modelsProvider', () => {
|
|||
description: 'Elastic Learned Sparse EncodeR v2',
|
||||
name: '.elser_model_2',
|
||||
version: 2,
|
||||
modelName: 'elser',
|
||||
},
|
||||
{
|
||||
arch: 'amd64',
|
||||
|
@ -72,6 +74,7 @@ describe('modelsProvider', () => {
|
|||
os: 'Linux',
|
||||
recommended: true,
|
||||
version: 2,
|
||||
modelName: 'elser',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -107,6 +110,7 @@ describe('modelsProvider', () => {
|
|||
hidden: true,
|
||||
name: '.elser_model_1',
|
||||
version: 1,
|
||||
modelName: 'elser',
|
||||
},
|
||||
{
|
||||
config: { input: { field_names: ['text_field'] } },
|
||||
|
@ -114,6 +118,7 @@ describe('modelsProvider', () => {
|
|||
description: 'Elastic Learned Sparse EncodeR v2',
|
||||
name: '.elser_model_2',
|
||||
version: 2,
|
||||
modelName: 'elser',
|
||||
},
|
||||
{
|
||||
arch: 'amd64',
|
||||
|
@ -122,6 +127,7 @@ describe('modelsProvider', () => {
|
|||
name: '.elser_model_2_linux-x86_64',
|
||||
os: 'Linux',
|
||||
version: 2,
|
||||
modelName: 'elser',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -465,10 +465,10 @@ export class ModelsProvider {
|
|||
(isCloud && def.os === 'Linux' && def.arch === 'amd64') ||
|
||||
(sameArch && !!def?.os && def?.os === osName && def?.arch === arch);
|
||||
|
||||
const { modelName, ...rest } = def;
|
||||
const { modelName } = def;
|
||||
|
||||
const modelDefinitionResponse = {
|
||||
...rest,
|
||||
...def,
|
||||
...(recommended ? { recommended } : {}),
|
||||
name,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue