Skip to content
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

Merged
merged 8 commits into from
Nov 21, 2023
9 changes: 9 additions & 0 deletions x-pack/plugins/ml/common/types/trained_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TotalFeatureImportance,
} from '@kbn/ml-data-frame-analytics-utils';
import { IndexName, IndicesIndexState } from '@elastic/elasticsearch/lib/api/types';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { XOR } from './common';
import type { MlSavedObjectType } from './saved_objects';

Expand Down Expand Up @@ -98,6 +99,7 @@ export type TrainedModelConfigResponse = estypes.MlTrainedModelConfig & {
* Associated pipelines. Extends response from the ES endpoint.
*/
pipelines?: Record<string, PipelineDefinition> | null;
origin_job_exists?: boolean;
Copy link
Member

@jgowdyelastic jgowdyelastic Nov 13, 2023

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 7c06abd


metadata?: {
analytics_config: DataFrameAnalyticsConfig;
Expand Down Expand Up @@ -290,3 +292,10 @@ export interface TrainedModelStatsResponse extends estypes.MlTrainedModelStats {
deployment_stats?: Omit<TrainedModelDeploymentStatsResponse, 'model_id'>;
model_size_stats?: TrainedModelModelSizeStats;
}

export function isTrainedModelConfigResponse(arg: unknown): arg is TrainedModelConfigResponse {
return (
isPopulatedObject(arg, ['metadata']) &&
isPopulatedObject(arg.metadata ?? {}, ['analytics_config'])
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 as casting later on? (Saw these lines were copied as is but maybe it's an easy fix)


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,
Expand All @@ -54,7 +129,6 @@ export function useModelActions({
}): Array<Action<ModelItem>> {
const {
services: {
application: { navigateToUrl },
overlays,
theme,
i18n: i18nStart,
Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a tooltip to show why the action is disabled.

Copy link
Contributor Author

@alvarezmelissa87 alvarezmelissa87 Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 6d8a4e5
Happy to get suggestions on the copy to use.
image

isPrimary: true,
},
{
Expand Down Expand Up @@ -606,7 +651,6 @@ export function useModelActions({
isLoading,
modelAndDeploymentIds,
navigateToPath,
navigateToUrl,
onDfaTestAction,
onLoading,
onModelDeployRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type ModelItem = TrainedModelConfigResponse & {
type?: string[];
stats?: Stats & { deployment_stats: TrainedModelDeploymentStatsResponse[] };
pipelines?: ModelPipelines['pipelines'] | null;
origin_job_exists?: boolean;
deployment_ids: string[];
putModelConfig?: object;
state: ModelState;
Expand Down
49 changes: 45 additions & 4 deletions x-pack/plugins/ml/server/routes/trained_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import {
createIngestPipelineSchema,
modelDownloadsQuery,
} from './schemas/inference_schema';
import type {
import {
isTrainedModelConfigResponse,
PipelineDefinition,
TrainedModelConfigResponse,
type TrainedModelConfigResponse,
} from '../../common/types/trained_models';
import { mlLog } from '../lib/log';
import { forceQuerySchema } from './schemas/anomaly_detectors_schema';
Expand Down Expand Up @@ -191,10 +192,50 @@ export function trainedModelsRoutes(
mlLog.debug(e);
}

const body = filterForEnabledFeatureModels(result, getEnabledFeatures());
const filteredModels = filterForEnabledFeatureModels(result, getEnabledFeatures());

try {
// @ts-ignore
Copy link
Member

@jgowdyelastic jgowdyelastic Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types can be fixed by updating filterForEnabledFeatureModels to use a generic type. (I suggested this is a previous comment when we were thinking of the other implementation)
As result is TrainedModelConfigResponse and not estypes.MlTrainedModelConfig

export function filterForEnabledFeatureModels<
  T extends TrainedModelConfigResponse | estypes.MlTrainedModelConfig
>(models: T[], enabledFeatures: MlFeatures) {
  ...

This also means you don't need the isTrainedModelConfigResponse type guard later on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep - good catch - updated in 8c7094d

const jobIdsString = filteredModels.reduce(
(
jobIdsStr: string,
currentModel: TrainedModelConfigResponse | estypes.MlTrainedModelConfig,
idx: number
) => {
if (isTrainedModelConfigResponse(currentModel)) {
let id = currentModel.metadata?.analytics_config?.id ?? '';
if (id !== '') {
id = `${idx > 0 ? ',' : ''}${id}*`;
}
return `${jobIdsStr}${id}`;
}
return jobIdsStr;
},
''
);

if (jobIdsString !== '') {
const { data_frame_analytics: jobs } = await mlClient.getDataFrameAnalytics({
jgowdyelastic marked this conversation as resolved.
Show resolved Hide resolved
id: jobIdsString,
allow_no_match: true,
});

filteredModels.forEach((model) => {
if (isTrainedModelConfigResponse(model)) {
const dfaId = model?.metadata?.analytics_config?.id;
if (dfaId !== undefined) {
// if this is a dfa model, set origin_job_exists
model.origin_job_exists = jobs.find((job) => job.id === dfaId) !== undefined;
}
}
});
}
} 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
}

return response.ok({
body,
body: filteredModels,
});
} catch (e) {
return response.customError(wrapError(e));
Expand Down