[ML] Add map view for models in Trained Models and expand support for models in Analytics map (#162443)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2023-07-31 13:01:09 -05:00 committed by GitHub
parent 4d00959533
commit eebc0a4245
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 413 additions and 34 deletions

View file

@ -39,6 +39,7 @@ export const JOB_MAP_NODE_TYPES = {
TRANSFORM: 'transform',
INDEX: 'index',
TRAINED_MODEL: 'trainedModel',
INGEST_PIPELINE: 'ingestPipeline',
} as const;
/**

View file

@ -378,6 +378,7 @@ export interface AnalyticsMapNodeElement {
label: string;
type: string;
analysisType?: string;
isRoot?: boolean;
};
}

View file

@ -104,7 +104,6 @@ export function usePermissionCheck<T extends MlCapabilitiesKey | MlCapabilitiesK
mlCapabilitiesService.capabilities$,
mlCapabilitiesService.getCapabilities()
);
return useMemo(() => {
return Array.isArray(requestedCapabilities.current)
? requestedCapabilities.current.map((c) => capabilities[c])

View file

@ -206,6 +206,7 @@ export function AnalyticsIdSelector({
},
onSelectionChange: (selectedItem: TableItem[]) => {
const item = selectedItem[0];
if (!item) {
setSelected(undefined);
return;
@ -216,7 +217,7 @@ export function AnalyticsIdSelector({
setSelected({
model_id: isDFA ? undefined : item.model_id,
job_id: isDFA ? item.id : item.metadata?.analytics_config.id,
job_id: isDFA ? item.id : item.metadata?.analytics_config?.id,
analysis_type: analysisType,
});
},

View file

@ -11,6 +11,15 @@
display: 'inline-block';
}
.mlJobMapLegend__ingestPipeline {
height: $euiSizeM;
width: $euiSizeM;
background-color: $euiColorGhost;
border: $euiBorderWidthThick solid $euiColorVis7;
border-radius: $euiBorderRadiusSmall;
display: 'inline-block';
}
.mlJobMapLegend__transform {
height: $euiSizeM;
width: $euiSizeM;

View file

@ -31,8 +31,13 @@ import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils';
import { JOB_MAP_NODE_TYPES } from '@kbn/ml-data-frame-analytics-utils';
import { CytoscapeContext } from './cytoscape';
import { ML_PAGES } from '../../../../../../common/constants/locator';
import { checkPermission } from '../../../../capabilities/check_capabilities';
import { useMlLocator, useNotifications, useNavigateToPath } from '../../../../contexts/kibana';
import { usePermissionCheck } from '../../../../capabilities/check_capabilities';
import {
useMlLocator,
useNotifications,
useNavigateToPath,
useMlKibana,
} from '../../../../contexts/kibana';
import { getDataViewIdFromName } from '../../../../util/index_utils';
import { useNavigateToWizardWithClonedJob } from '../../analytics_management/components/action_clone/clone_action_name';
import {
@ -79,8 +84,8 @@ export const Controls: FC<Props> = React.memo(
const [isPopoverOpen, setPopover] = useState<boolean>(false);
const [didUntag, setDidUntag] = useState<boolean>(false);
const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics');
const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
const canCreateDataFrameAnalytics: boolean = usePermissionCheck('canCreateDataFrameAnalytics');
const canDeleteDataFrameAnalytics: boolean = usePermissionCheck('canDeleteDataFrameAnalytics');
const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics);
const {
closeDeleteJobCheckModal,
@ -93,6 +98,17 @@ export const Controls: FC<Props> = React.memo(
openModal,
openDeleteJobCheckModal,
} = deleteAction;
const {
services: {
share,
application: { navigateToUrl, capabilities },
},
} = useMlKibana();
const hasIngestPipelinesCapabilities =
capabilities.management?.ingest?.ingest_pipelines === true;
const { toasts } = useNotifications();
const mlLocator = useMlLocator()!;
const navigateToPath = useNavigateToPath();
@ -133,6 +149,19 @@ export const Controls: FC<Props> = React.memo(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeLabel]);
const onManagePipeline = useCallback(async () => {
const ingestPipelineLocator = share.url.locators.get('INGEST_PIPELINES_APP_LOCATOR');
if (ingestPipelineLocator && nodeLabel !== null) {
const path = await ingestPipelineLocator.getUrl({
page: 'pipeline_list',
});
// Passing pipelineId here because pipeline_list is not recognizing pipelineId params
await navigateToUrl(`${path}/?pipeline=${nodeLabel}`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [share.url.locators, nodeLabel]);
const onCloneJobClick = useCallback(async () => {
navigateToWizardWithClonedJob({ config: details[nodeId], stats: details[nodeId]?.stats });
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -262,6 +291,22 @@ export const Controls: FC<Props> = React.memo(
</EuiContextMenuItem>,
]
: []),
...(modelId !== nodeLabel &&
nodeType === JOB_MAP_NODE_TYPES.INGEST_PIPELINE &&
hasIngestPipelinesCapabilities
? [
<EuiContextMenuItem
key={`${nodeId}-view-pipeline`}
icon="pipelineApp"
onClick={onManagePipeline}
>
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.flyout.viewIngestPipelineButton"
defaultMessage="View ingest pipeline"
/>
</EuiContextMenuItem>,
]
: []),
];
return (
@ -298,7 +343,7 @@ export const Controls: FC<Props> = React.memo(
</EuiFlexGroup>
</EuiFlyoutBody>
<EuiFlyoutFooter>
{nodeType !== JOB_MAP_NODE_TYPES.TRAINED_MODEL && (
{nodeType !== JOB_MAP_NODE_TYPES.TRAINED_MODEL && items.length > 0 ? (
<EuiPopover
button={button}
isOpen={isPopoverOpen}
@ -308,7 +353,7 @@ export const Controls: FC<Props> = React.memo(
>
<EuiContextMenuPanel items={items} />
</EuiPopover>
)}
) : null}
</EuiFlyoutFooter>
</EuiFlyout>
{isDeleteJobCheckModalVisible && item && (

View file

@ -19,8 +19,8 @@ import { css } from '@emotion/react';
import cytoscape, { type Stylesheet } from 'cytoscape';
// @ts-ignore no declaration file
import dagre from 'cytoscape-dagre';
import { EuiThemeType } from '../../../../components/color_range_legend';
import { getCytoscapeOptions } from './cytoscape_options';
import { EuiThemeType } from '../../../../components/color_range_legend';
cytoscape.use(dagre);
@ -98,8 +98,8 @@ export function Cytoscape({
// Add the height to the div style. The height is a separate prop because it
// is required and can trigger rendering when changed.
const divStyle = useMemo(() => {
return { ...style, height };
}, [style, height]);
return { ...style, height, width };
}, [style, height, width]);
const dataHandler = useCallback<cytoscape.EventHandler>(
(event) => {
@ -144,8 +144,10 @@ export function Cytoscape({
useEffect(() => {
if (cy) {
cy.reset();
// Refitting because it's possible the the width/height have changed
cy.fit();
}
}, [cy, resetCy]);
}, [cy, resetCy, width, height]);
return (
<CytoscapeContext.Provider value={cy}>

View file

@ -19,6 +19,7 @@ const MAP_SHAPES = {
RECTANGLE: 'rectangle',
DIAMOND: 'diamond',
TRIANGLE: 'triangle',
ROUND_RECTANGLE: 'round-rectangle',
} as const;
type MapShapes = typeof MAP_SHAPES[keyof typeof MAP_SHAPES];
@ -33,6 +34,9 @@ function shapeForNode(el: cytoscape.NodeSingular, theme: EuiThemeType): MapShape
return MAP_SHAPES.DIAMOND;
case JOB_MAP_NODE_TYPES.TRAINED_MODEL:
return MAP_SHAPES.TRIANGLE;
case JOB_MAP_NODE_TYPES.INGEST_PIPELINE:
return MAP_SHAPES.ROUND_RECTANGLE;
default:
return MAP_SHAPES.ELLIPSE;
}
@ -69,6 +73,9 @@ function borderColorForNode(el: cytoscape.NodeSingular, theme: EuiThemeType) {
return theme.euiColorVis2;
case JOB_MAP_NODE_TYPES.TRAINED_MODEL:
return theme.euiColorVis3;
case JOB_MAP_NODE_TYPES.INGEST_PIPELINE:
return theme.euiColorVis7;
default:
return theme.euiColorMediumShade;
}

View file

@ -71,6 +71,21 @@ export const JobMapLegend: FC<{ theme: EuiThemeType }> = ({ theme }) => {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<span className="mlJobMapLegend__ingestPipeline" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.legend.ingestPipelineLabel"
defaultMessage="ingest pipeline"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>

View file

@ -34,17 +34,18 @@ linear-gradient(
center,
${theme.euiColorLightShade}`,
backgroundSize: `${theme.euiSizeL} ${theme.euiSizeL}`,
margin: `-${theme.euiSizeL}`,
marginTop: 0,
});
interface Props {
key?: string;
defaultHeight?: number;
analyticsId?: string;
modelId?: string;
forceRefresh?: boolean;
}
export const JobMap: FC<Props> = ({ analyticsId, modelId, forceRefresh }) => {
export const JobMap: FC<Props> = ({ defaultHeight, analyticsId, modelId, forceRefresh }) => {
// itemsDeleted will reset to false when Controls component calls updateElements to remove nodes deleted from map
const [itemsDeleted, setItemsDeleted] = useState<boolean>(false);
const [resetCyToggle, setResetCyToggle] = useState<boolean>(false);
@ -150,6 +151,7 @@ export const JobMap: FC<Props> = ({ analyticsId, modelId, forceRefresh }) => {
const refreshCallback = () => fetchAndSetElementsWrapper({ analyticsId, modelId });
const h = defaultHeight ?? height;
return (
<div data-test-subj="mlPageDataFrameAnalyticsMap">
<EuiSpacer size="m" />
@ -171,10 +173,10 @@ export const JobMap: FC<Props> = ({ analyticsId, modelId, forceRefresh }) => {
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<div style={{ height: height - parseInt(euiTheme.euiSizeL, 10) - 20 }} ref={ref}>
<div style={{ height: h - parseInt(euiTheme.euiSizeL, 10) - 20 }} ref={ref}>
<Cytoscape
theme={euiTheme}
height={height - 20}
height={h - 20}
elements={elements}
width={width}
style={getCytoscapeDivStyle(euiTheme)}

View file

@ -27,6 +27,7 @@ import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import { TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils';
import { JobMap } from '../data_frame_analytics/pages/job_map';
import type { ModelItemFull } from './models_list';
import { ModelPipelines } from './pipelines';
import { AllocatedModels } from '../memory_usage/nodes_overview/allocated_models';
@ -419,6 +420,29 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
},
]
: []),
{
id: 'models_map',
'data-test-subj': 'mlTrainedModelsMap',
name: (
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.expandedRow.modelsMapLabel"
defaultMessage="Models map"
/>
),
content: (
<div data-test-subj={'mlTrainedModelDetailsContent'}>
<EuiSpacer size={'s'} />
<EuiFlexItem css={{ height: 300 }}>
<JobMap
analyticsId={undefined}
modelId={item.model_id}
forceRefresh={false}
defaultHeight={200}
/>
</EuiFlexItem>
</div>
),
},
];
}, [
analyticsConfig,
@ -430,6 +454,7 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
pipelines,
restMetaData,
stats,
item.model_id,
]);
const initialSelectedTab =

View file

@ -18,6 +18,10 @@ import {
type AnalyticsMapNodeElement,
type MapElements,
} from '@kbn/ml-data-frame-analytics-utils';
import type { TransformGetTransformTransformSummary } from '@elastic/elasticsearch/lib/api/types';
import { flatten } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { modelsProvider } from '../model_management';
import {
ExtendAnalyticsMapArgs,
GetAnalyticsMapArgs,
@ -29,12 +33,15 @@ import {
isJobDataLinkReturnType,
isTransformLinkReturnType,
NextLinkReturnType,
GetAnalyticsJobIdArg,
GetAnalyticsModelIdArg,
} from './types';
import type { MlClient } from '../../lib/ml_client';
export class AnalyticsManager {
private _trainedModels: estypes.MlTrainedModelConfig[] = [];
private _jobs: estypes.MlDataframeAnalyticsSummary[] = [];
private _transforms?: TransformGetTransformTransformSummary[];
constructor(private _mlClient: MlClient, private _client: IScopedClusterClient) {}
@ -47,6 +54,30 @@ export class AnalyticsManager {
this._jobs = jobs.data_frame_analytics;
}
private async initTransformData() {
if (!this._transforms) {
try {
const body = await this._client.asCurrentUser.transform.getTransform({
size: 1000,
});
this._transforms = body.transforms;
return body.transforms;
} catch (e) {
if (e.meta?.statusCode !== 403) {
// eslint-disable-next-line no-console
console.error(e);
}
}
}
}
private getNodeId(
elementOriginalId: string,
nodeType: typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]
): string {
return `${elementOriginalId}-${nodeType}`;
}
private isDuplicateElement(analyticsId: string, elements: MapElements[]): boolean {
let isDuplicate = false;
elements.forEach((elem) => {
@ -310,7 +341,7 @@ export class AnalyticsManager {
* @param jobId (optional)
* @param modelId (optional)
*/
public async getAnalyticsMap({
private async getAnalyticsMap({
analyticsId,
modelId,
}: GetAnalyticsMapArgs): Promise<AnalyticsMapReturnType> {
@ -527,6 +558,240 @@ export class AnalyticsManager {
}
}
/**
* Expanded wrapper of getAnalyticsMap, which also handles generic models that are not tied to an analytics job
* Retrieves info about model and ingest pipeline, index, and transforms associated with the model
* @param analyticsId
* @param modelId
*/
public async extendModelsMap({
analyticsId,
modelId,
}: {
analyticsId?: string;
modelId?: string;
}): Promise<AnalyticsMapReturnType> {
const result: AnalyticsMapReturnType = {
elements: [],
details: {},
error: null,
};
try {
if (analyticsId && !modelId) {
return this.getAnalyticsMap({
analyticsId,
modelId,
} as GetAnalyticsJobIdArg);
}
await this.initData();
const modelNodeId = `${modelId}-${JOB_MAP_NODE_TYPES.TRAINED_MODEL}`;
const model = modelId ? this.findTrainedModel(modelId) : undefined;
const isDFAModel = isPopulatedObject(model?.metadata, ['analytics_config']);
if (isDFAModel) {
return this.getAnalyticsMap({
analyticsId,
modelId,
} as GetAnalyticsModelIdArg);
}
if (modelId && model) {
// First, find information about the trained model
result.elements.push({
data: {
id: modelNodeId,
label: modelId,
type: JOB_MAP_NODE_TYPES.TRAINED_MODEL,
isRoot: true,
},
});
result.details[modelNodeId] = model;
let pipelinesResponse;
let indicesSettings;
try {
// Then, find the pipelines that have the trained model set as index.default_pipelines
pipelinesResponse = await modelsProvider(this._client).getModelsPipelines([modelId]);
} catch (e) {
// Possible that the user doesn't have permissions to view ingest pipelines
// If so, gracefully exit
if (e.meta?.statusCode !== 403) {
// eslint-disable-next-line no-console
console.error(e);
}
return result;
}
const pipelines = pipelinesResponse?.get(modelId);
if (pipelines) {
const pipelineIds = new Set(Object.keys(pipelines));
for (const pipelineId of pipelineIds) {
const pipelineNodeId = `${pipelineId}-${JOB_MAP_NODE_TYPES.INGEST_PIPELINE}`;
result.details[pipelineNodeId] = pipelines[pipelineId];
result.elements.push({
data: {
id: pipelineNodeId,
label: pipelineId,
type: JOB_MAP_NODE_TYPES.INGEST_PIPELINE,
},
});
result.elements.push({
data: {
id: `${modelNodeId}~${pipelineNodeId}`,
source: modelNodeId,
target: pipelineNodeId,
},
});
}
const pipelineIdsToDestinationIndices: Record<string, string[]> = {};
let indicesPermissions;
try {
indicesSettings = await this._client.asInternalUser.indices.getSettings();
const hasPrivilegesResponse = await this._client.asCurrentUser.security.hasPrivileges({
index: [
{
names: Object.keys(indicesSettings),
privileges: ['read'],
},
],
});
indicesPermissions = hasPrivilegesResponse.index;
} catch (e) {
// Possible that the user doesn't have permissions to view
// If so, gracefully exit
if (e.meta?.statusCode !== 403) {
// eslint-disable-next-line no-console
console.error(e);
}
return result;
}
for (const [indexName, { settings }] of Object.entries(indicesSettings)) {
if (
settings?.index?.default_pipeline &&
pipelineIds.has(settings.index.default_pipeline) &&
indicesPermissions[indexName]?.read === true
) {
if (Array.isArray(pipelineIdsToDestinationIndices[settings.index.default_pipeline])) {
pipelineIdsToDestinationIndices[settings.index.default_pipeline].push(indexName);
} else {
pipelineIdsToDestinationIndices[settings.index.default_pipeline] = [indexName];
}
}
}
for (const [pipelineId, indexIds] of Object.entries(pipelineIdsToDestinationIndices)) {
const pipelineNodeId = this.getNodeId(pipelineId, JOB_MAP_NODE_TYPES.INGEST_PIPELINE);
for (const destinationIndexId of indexIds) {
const destinationIndexNodeId = this.getNodeId(
destinationIndexId,
JOB_MAP_NODE_TYPES.INDEX
);
const destinationIndexDetails = await this.getIndexData(destinationIndexId);
result.details[destinationIndexNodeId] = {
...destinationIndexDetails,
ml_inference_models: [modelId],
};
result.elements.push({
data: {
id: destinationIndexNodeId,
label: destinationIndexId,
type: JOB_MAP_NODE_TYPES.INDEX,
},
});
result.elements.push({
data: {
id: `${pipelineNodeId}~${destinationIndexNodeId}`,
source: pipelineNodeId,
target: destinationIndexNodeId,
},
});
}
}
const destinationIndices = flatten(Object.values(pipelineIdsToDestinationIndices));
// From these destination indices, see if there's any transforms that have the indexId as the source destination index
if (destinationIndices.length > 0) {
const transforms = await this.initTransformData();
if (!transforms) return result;
for (const destinationIndex of destinationIndices) {
const destinationIndexNodeId = `${destinationIndex}-${JOB_MAP_NODE_TYPES.INDEX}`;
const foundTransform = transforms?.find((t) => {
const transformSourceIndex = Array.isArray(t.source.index)
? t.source.index[0]
: t.source.index;
return transformSourceIndex === destinationIndex;
});
if (foundTransform) {
const transformDestIndex = foundTransform.dest.index;
const transformNodeId = `${foundTransform.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`;
const transformDestIndexNodeId = `${transformDestIndex}-${JOB_MAP_NODE_TYPES.INDEX}`;
const destIndex = await this.getIndexData(transformDestIndex);
result.details[transformNodeId] = foundTransform;
result.details[transformDestIndexNodeId] = destIndex;
result.elements.push(
{
data: {
id: transformNodeId,
label: foundTransform.id,
type: JOB_MAP_NODE_TYPES.TRANSFORM,
},
},
{
data: {
id: transformDestIndexNodeId,
label: transformDestIndex,
type: JOB_MAP_NODE_TYPES.INDEX,
},
}
);
result.elements.push(
{
data: {
id: `${destinationIndexNodeId}~${transformNodeId}`,
source: destinationIndexNodeId,
target: transformNodeId,
},
},
{
data: {
id: `${transformNodeId}~${transformDestIndexNodeId}`,
source: transformNodeId,
target: transformDestIndexNodeId,
},
}
);
}
}
}
}
}
} catch (error) {
result.error = error.message || 'An error occurred fetching map';
return result;
}
return result;
}
public async extendAnalyticsMapForAnalyticsJob({
analyticsId,
index,

View file

@ -15,10 +15,10 @@ import type {
interface AnalyticsMapArg {
analyticsId: string;
}
interface GetAnalyticsJobIdArg extends AnalyticsMapArg {
export interface GetAnalyticsJobIdArg extends AnalyticsMapArg {
modelId?: never;
}
interface GetAnalyticsModelIdArg {
export interface GetAnalyticsModelIdArg {
analyticsId?: never;
modelId: string;
}

View file

@ -30,10 +30,7 @@ import {
analyticsNewJobCapsParamsSchema,
analyticsNewJobCapsQuerySchema,
} from './schemas/data_analytics_schema';
import type {
GetAnalyticsMapArgs,
ExtendAnalyticsMapArgs,
} from '../models/data_frame_analytics/types';
import type { ExtendAnalyticsMapArgs } from '../models/data_frame_analytics/types';
import { DataViewHandler } from '../models/data_frame_analytics/index_patterns';
import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manager';
import { validateAnalyticsJob } from '../models/data_frame_analytics/validation';
@ -51,15 +48,6 @@ function deleteDestDataViewById(dataViewsService: DataViewsService, dataViewId:
return iph.deleteDataViewById(dataViewId);
}
function getAnalyticsMap(
mlClient: MlClient,
client: IScopedClusterClient,
idOptions: GetAnalyticsMapArgs
) {
const analytics = new AnalyticsManager(mlClient, client);
return analytics.getAnalyticsMap(idOptions);
}
function getExtendedMap(
mlClient: MlClient,
client: IScopedClusterClient,
@ -69,6 +57,23 @@ function getExtendedMap(
return analytics.extendAnalyticsMapForAnalyticsJob(idOptions);
}
function getExtendedModelsMap(
mlClient: MlClient,
client: IScopedClusterClient,
idOptions: {
analyticsId?: string;
modelId?: string;
}
) {
const analytics = new AnalyticsManager(mlClient, client);
return analytics.extendModelsMap(idOptions);
}
export function getAnalyticsManager(mlClient: MlClient, client: IScopedClusterClient) {
const analytics = new AnalyticsManager(mlClient, client);
return analytics;
}
// replace the recursive field and agg references with a
// map of ids to allow it to be stringified for transportation
// over the network.
@ -768,6 +773,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout
.get({
path: `${ML_INTERNAL_BASE_PATH}/data_frame/analytics/map/{analyticsId}`,
access: 'internal',
options: {
tags: ['access:ml:canGetDataFrameAnalytics'],
},
})
.addVersion(
{
@ -793,8 +801,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout
index: type === JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined,
});
} else {
// @ts-expect-error never used as analyticsId
results = await getAnalyticsMap(mlClient, client, {
results = await getExtendedModelsMap(mlClient, client, {
analyticsId: type !== JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined,
modelId: type === JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined,
});