mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
4d00959533
commit
eebc0a4245
14 changed files with 413 additions and 34 deletions
|
@ -39,6 +39,7 @@ export const JOB_MAP_NODE_TYPES = {
|
|||
TRANSFORM: 'transform',
|
||||
INDEX: 'index',
|
||||
TRAINED_MODEL: 'trainedModel',
|
||||
INGEST_PIPELINE: 'ingestPipeline',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
|
@ -378,6 +378,7 @@ export interface AnalyticsMapNodeElement {
|
|||
label: string;
|
||||
type: string;
|
||||
analysisType?: string;
|
||||
isRoot?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue