-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ML] Trained models list: disables 'View training data' action if data frame analytics job no longer exists #171061
Changes from 4 commits
a407ee2
7c06abd
6d8a4e5
12f1a01
50ffcfd
8c7094d
3bc2c8d
73216d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,9 +7,10 @@ | |
|
||
import { Action } from '@elastic/eui/src/components/basic_table/action_types'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { FormattedMessage } from '@kbn/i18n-react'; | ||
import { isPopulatedObject } from '@kbn/ml-is-populated-object'; | ||
import { EuiToolTip } from '@elastic/eui'; | ||
import React, { useCallback, useMemo, useEffect, useState } from 'react'; | ||
import { EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; | ||
import React, { FC, useCallback, useMemo, useEffect, useState } from 'react'; | ||
import { | ||
BUILT_IN_MODEL_TAG, | ||
DEPLOYMENT_STATE, | ||
|
@@ -33,6 +34,80 @@ import { isTestable, isDfaTrainedModel } from './test_models'; | |
import { ModelItem } from './models_list'; | ||
import { usePermissionCheck } from '../capabilities/check_capabilities'; | ||
|
||
const ViewTrainingDataAction: FC<{ | ||
item: ModelItem; | ||
}> = ({ item }) => { | ||
const urlLocator = useMlLocator()!; | ||
const { | ||
services: { | ||
application: { navigateToUrl }, | ||
}, | ||
} = useMlKibana(); | ||
|
||
const handleClick = async () => { | ||
if (item.metadata?.analytics_config === undefined) return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Could this be a check probably with a type guard so we can avoid the |
||
|
||
const analysisType = getAnalysisType( | ||
item.metadata?.analytics_config.analysis | ||
) as DataFrameAnalysisConfigType; | ||
|
||
const url = await urlLocator.getUrl({ | ||
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, | ||
pageState: { | ||
jobId: item.metadata?.analytics_config.id as string, | ||
analysisType, | ||
...(analysisType === 'classification' || analysisType === 'regression' | ||
? { | ||
queryText: `${item.metadata?.analytics_config.dest.results_field}.is_training : true`, | ||
} | ||
: {}), | ||
}, | ||
}); | ||
|
||
await navigateToUrl(url); | ||
}; | ||
|
||
const buttonContent = ( | ||
peteharverson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<> | ||
<EuiIcon type={'visTable'} css={{ marginRight: '8px' }} /> | ||
{i18n.translate('xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel', { | ||
defaultMessage: 'View training data', | ||
})} | ||
</> | ||
); | ||
|
||
const isEnabled = item.origin_job_exists; | ||
const button = isEnabled ? ( | ||
<EuiLink onClick={handleClick} color={'text'}> | ||
<EuiText size="s" color={'text'}> | ||
{buttonContent} | ||
</EuiText> | ||
</EuiLink> | ||
) : ( | ||
<EuiText size="s" color={'subdued'} css={{ fontWeight: 300 }}> | ||
{buttonContent} | ||
</EuiText> | ||
); | ||
|
||
if (isEnabled) { | ||
return button; | ||
} else { | ||
return ( | ||
<EuiToolTip | ||
position="top" | ||
content={ | ||
<FormattedMessage | ||
id="xpack.ml.trainedModels.modelsList.viewTrainingDataActionTooltip" | ||
defaultMessage="Related data frame analytics job was not found." | ||
/> | ||
} | ||
> | ||
{button} | ||
</EuiToolTip> | ||
); | ||
} | ||
}; | ||
|
||
export function useModelActions({ | ||
onDfaTestAction, | ||
onTestAction, | ||
|
@@ -54,7 +129,6 @@ export function useModelActions({ | |
}): Array<Action<ModelItem>> { | ||
const { | ||
services: { | ||
application: { navigateToUrl }, | ||
overlays, | ||
theme, | ||
i18n: i18nStart, | ||
|
@@ -130,40 +204,11 @@ export function useModelActions({ | |
return useMemo( | ||
() => [ | ||
{ | ||
name: i18n.translate('xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel', { | ||
defaultMessage: 'View training data', | ||
}), | ||
description: i18n.translate( | ||
'xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel', | ||
{ | ||
defaultMessage: 'View training data', | ||
} | ||
), | ||
icon: 'visTable', | ||
type: 'icon', | ||
available: (item) => !!item.metadata?.analytics_config?.id, | ||
onClick: async (item) => { | ||
if (item.metadata?.analytics_config === undefined) return; | ||
|
||
const analysisType = getAnalysisType( | ||
item.metadata?.analytics_config.analysis | ||
) as DataFrameAnalysisConfigType; | ||
|
||
const url = await urlLocator.getUrl({ | ||
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, | ||
pageState: { | ||
jobId: item.metadata?.analytics_config.id as string, | ||
analysisType, | ||
...(analysisType === 'classification' || analysisType === 'regression' | ||
? { | ||
queryText: `${item.metadata?.analytics_config.dest.results_field}.is_training : true`, | ||
} | ||
: {}), | ||
}, | ||
}); | ||
|
||
await navigateToUrl(url); | ||
render: (item) => { | ||
return <ViewTrainingDataAction item={item} />; | ||
}, | ||
available: (item) => !!item.metadata?.analytics_config?.id, | ||
enabled: (item) => item.origin_job_exists === true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a tooltip to show why the action is disabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in 6d8a4e5 |
||
isPrimary: true, | ||
}, | ||
{ | ||
|
@@ -606,7 +651,6 @@ export function useModelActions({ | |
isLoading, | ||
modelAndDeploymentIds, | ||
navigateToPath, | ||
navigateToUrl, | ||
onDfaTestAction, | ||
onLoading, | ||
onModelDeployRequest, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -190,11 +190,41 @@ export function trainedModelsRoutes( | |||||||||||
// we don't need to fill kibana's log with these messages. | ||||||||||||
mlLog.debug(e); | ||||||||||||
} | ||||||||||||
const enabledFeatures = getEnabledFeatures(); | ||||||||||||
try { | ||||||||||||
if (enabledFeatures.dfa) { | ||||||||||||
const jobIds = result.map((model) => { | ||||||||||||
let id = model.metadata?.analytics_config?.id; | ||||||||||||
if (id) { | ||||||||||||
id = `${id}*`; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should be modifying the IDs. This could lead to unexpected behaviour. e.g. if we have two jobs In my previous suggestion I overlooked the fact that comma separating the IDs to pass to I still don't think we should call I had a go at writing up what I'm thinking. I got carried away trying to make it performant. const filteredModels = filterForEnabledFeatureModels<TrainedModelConfigResponse>(
result,
getEnabledFeatures()
);
const dfaJobIdMap = filteredModels.reduce<Record<string, string>>((c, m) => {
const id = m.metadata?.analytics_config?.id;
if (id !== undefined) {
c[m.model_id] = id;
}
return c;
}, {});
const jobIds = Object.values(dfaJobIdMap);
if (jobIds.length === 0) {
// return early, there are no dfa jobs
return response.ok({
body: filteredModels,
});
}
let dfaJobs: estypes.MlDataframeAnalyticsSummary[] = [];
try {
const jobs =
jobIds.length === 1
? await mlClient.getDataFrameAnalytics({
id: jobIds[0],
})
: await mlClient.getDataFrameAnalytics();
dfaJobs = jobs.data_frame_analytics;
} catch (e) {
//
}
for (const model of filteredModels) {
const dfaJob = dfaJobs.find((j) => j.id === dfaJobIdMap[model.model_id]);
model.origin_job_exists = dfaJob !== undefined;
}
return response.ok({
body: filteredModels,
}); Also updating export function filterForEnabledFeatureModels<
T extends TrainedModelConfigResponse | estypes.MlTrainedModelConfig
>(models: T[], enabledFeatures: MlFeatures) {
... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've discussed this offline and can't think of a situation where adding |
||||||||||||
} | ||||||||||||
return id; | ||||||||||||
}); | ||||||||||||
const filteredJobIds = jobIds.filter((id) => id !== undefined); | ||||||||||||
|
||||||||||||
const body = filterForEnabledFeatureModels(result, getEnabledFeatures()); | ||||||||||||
const { data_frame_analytics: jobs } = await mlClient.getDataFrameAnalytics({ | ||||||||||||
jgowdyelastic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
id: filteredJobIds.join(','), | ||||||||||||
jgowdyelastic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
allow_no_match: true, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
jobs.forEach(({ id }) => { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be looping through models, not jobs. We need to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the job hasn't been returned then the I was originally thinking that it would be more efficient to just loop through the returned jobs since that would mean maybe we wouldn't need to go through all the models but happy to change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to loop through the filtered models to ensure all dfa models get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was actually thinking that to make this more efficient we could keep a list of the dfa models when identifying them in the reduce and only loop through those later on. e.g. const dfaModels = [];
const jobIdsString = filteredModels.reduce(
(jobIdsStr: string, currentModel: TrainedModelConfigResponse, idx: number) => {
if (isTrainedModelConfigResponse(currentModel)) {
dfaModels.push(currentModel);
.... But it's not needed, the time difference will be milliseconds |
||||||||||||
const model = result.find( | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to explicitly set to
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would only set it to true if a job id returned from the check matched the job id on the model. But agree that we should be setting it explicitly to false instead of just not adding the property at all and relying on falsey-ness. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to set explicitly in 50ffcfd There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly yeah, |
||||||||||||
(modelWithJob) => id === modelWithJob.metadata?.analytics_config?.id | ||||||||||||
); | ||||||||||||
|
||||||||||||
if (model) { | ||||||||||||
model.origin_job_exists = true; | ||||||||||||
} | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
} catch (e) { | ||||||||||||
// Swallow error to prevent blocking trained models result | ||||||||||||
jgowdyelastic marked this conversation as resolved.
Show resolved
Hide resolved
peteharverson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
} | ||||||||||||
|
||||||||||||
const filteredModels = filterForEnabledFeatureModels(result, enabledFeatures); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can loop over the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep - that makes sense - updated in 50ffcfd |
||||||||||||
|
||||||||||||
return response.ok({ | ||||||||||||
body, | ||||||||||||
body: filteredModels, | ||||||||||||
}); | ||||||||||||
} catch (e) { | ||||||||||||
return response.customError(wrapError(e)); | ||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this needs to be optional route param. We should always perform this check when retrieving the models as it's useful information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in 7c06abd