Skip to content

Commit 00e5951

Browse files
[ML] Data frame analytics: Adds functionality to map view (#83710)
* get all jobs from index node * create map from modelId and enable url share * highlight source node * add map endpoint to api doc * use variables in css.fix types.ensure map tab is shown * fix translations
1 parent 22e494e commit 00e5951

File tree

17 files changed

+564
-281
lines changed

17 files changed

+564
-281
lines changed

x-pack/plugins/ml/common/constants/data_frame_analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const JOB_MAP_NODE_TYPES = {
1616
ANALYTICS: 'analytics',
1717
TRANSFORM: 'transform',
1818
INDEX: 'index',
19-
INFERENCE_MODEL: 'inferenceModel',
19+
TRAINED_MODEL: 'trainedModel',
2020
} as const;
2121

2222
export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES];

x-pack/plugins/ml/common/types/ml_url_generator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export type TimeSeriesExplorerUrlState = MLPageState<
156156

157157
export interface DataFrameAnalyticsQueryState {
158158
jobId?: JobId | JobId[];
159+
modelId?: string;
159160
groupIds?: string[];
160161
globalState?: MlCommonGlobalState;
161162
}
@@ -170,6 +171,7 @@ export interface DataFrameAnalyticsExplorationQueryState {
170171
jobId: JobId;
171172
analysisType: DataFrameAnalysisConfigType;
172173
defaultIsTraining?: boolean;
174+
modelId?: string;
173175
};
174176
}
175177

@@ -180,6 +182,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState<
180182
analysisType: DataFrameAnalysisConfigType;
181183
globalState?: MlCommonGlobalState;
182184
defaultIsTraining?: boolean;
185+
modelId?: string;
183186
}
184187
>;
185188

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ interface Tab {
1515
path: string;
1616
}
1717

18-
export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({
19-
jobId,
20-
selectedTabId,
21-
}) => {
18+
export const AnalyticsNavigationBar: FC<{
19+
selectedTabId?: string;
20+
jobId?: string;
21+
modelId?: string;
22+
}> = ({ jobId, modelId, selectedTabId }) => {
2223
const navigateToPath = useNavigateToPath();
2324

2425
const tabs = useMemo(() => {
@@ -38,7 +39,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string
3839
path: '/data_frame_analytics/models',
3940
},
4041
];
41-
if (jobId !== undefined) {
42+
if (jobId !== undefined || modelId !== undefined) {
4243
navTabs.push({
4344
id: 'map',
4445
name: i18n.translate('xpack.ml.dataframe.mapTabLabel', {

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export const ModelsList: FC = () => {
342342
onClick: async (item) => {
343343
const path = await mlUrlGenerator.createUrl({
344344
page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP,
345-
pageState: { jobId: item.metadata?.analytics_config.id },
345+
pageState: { modelId: item.model_id },
346346
});
347347

348348
await navigateToPath(path, false);

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const Page: FC = () => {
5959
const location = useLocation();
6060
const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]);
6161
const mapJobId = globalState?.ml?.jobId;
62+
const mapModelId = globalState?.ml?.modelId;
6263

6364
return (
6465
<Fragment>
@@ -106,8 +107,14 @@ export const Page: FC = () => {
106107
<UpgradeWarning />
107108

108109
<EuiPageContent>
109-
<AnalyticsNavigationBar selectedTabId={selectedTabId} jobId={mapJobId} />
110-
{selectedTabId === 'map' && mapJobId && <JobMap analyticsId={mapJobId} />}
110+
<AnalyticsNavigationBar
111+
selectedTabId={selectedTabId}
112+
jobId={mapJobId}
113+
modelId={mapModelId}
114+
/>
115+
{selectedTabId === 'map' && (mapJobId || mapModelId) && (
116+
<JobMap analyticsId={mapJobId} modelId={mapModelId} />
117+
)}
111118
{selectedTabId === 'data_frame_analytics' && (
112119
<DataFrameAnalyticsList
113120
blockRefresh={blockRefresh}

x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
.mlJobMapLegend__indexPattern {
66
height: $euiSizeM;
77
width: $euiSizeM;
8-
background-color: '#FFFFFF';
8+
background-color: $euiColorGhost;
99
border: 1px solid $euiColorVis2;
1010
transform: rotate(45deg);
1111
display: 'inline-block';
@@ -14,25 +14,34 @@
1414
.mlJobMapLegend__transform {
1515
height: $euiSizeM;
1616
width: $euiSizeM;
17-
background-color: '#FFFFFF';
17+
background-color: $euiColorGhost;
1818
border: 1px solid $euiColorVis1;
1919
display: 'inline-block';
2020
}
2121

2222
.mlJobMapLegend__analytics {
2323
height: $euiSizeM;
2424
width: $euiSizeM;
25-
background-color: '#FFFFFF';
25+
background-color: $euiColorGhost;
2626
border: 1px solid $euiColorVis0;
27-
border-radius: 50%;
27+
border-radius: $euiBorderRadius;
2828
display: 'inline-block';
2929
}
3030

31-
.mlJobMapLegend__inferenceModel {
31+
.mlJobMapLegend__trainedModel {
3232
height: $euiSizeM;
3333
width: $euiSizeM;
34-
background-color: '#FFFFFF';
35-
border: 1px solid $euiColorMediumShade;
36-
border-radius: 50%;
34+
background-color: $euiColorGhost;
35+
border: $euiBorderThin;
36+
border-radius: $euiBorderRadius;
37+
display: 'inline-block';
38+
}
39+
40+
.mlJobMapLegend__sourceNode {
41+
height: $euiSizeM;
42+
width: $euiSizeM;
43+
background-color: $euiColorLightShade;
44+
border: $euiBorderThin;
45+
border-radius: $euiBorderRadius;
3746
display: 'inline-block';
3847
}

x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description
2525
import { CytoscapeContext } from './cytoscape';
2626
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/util/date_utils';
2727
import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics';
28-
// import { DeleteButton } from './delete_button';
28+
// import { DeleteButton } from './delete_button'; // TODO: add delete functionality in followup
2929

3030
interface Props {
31-
analyticsId: string;
31+
analyticsId?: string;
32+
modelId?: string;
3233
details: any;
3334
getNodeData: any;
3435
}
@@ -56,7 +57,7 @@ function getListItems(details: object): EuiDescriptionListProps['listItems'] {
5657
});
5758
}
5859

59-
export const Controls: FC<Props> = ({ analyticsId, details, getNodeData }) => {
60+
export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData }) => {
6061
const [showFlyout, setShowFlyout] = useState<boolean>(false);
6162
const [selectedNode, setSelectedNode] = useState<cytoscape.NodeSingular | undefined>();
6263

@@ -98,10 +99,12 @@ export const Controls: FC<Props> = ({ analyticsId, details, getNodeData }) => {
9899
}
99100

100101
const nodeDataButton =
101-
analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? (
102+
analyticsId !== nodeLabel &&
103+
modelId !== nodeLabel &&
104+
(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX) ? (
102105
<EuiButtonEmpty
103106
onClick={() => {
104-
getNodeData(nodeLabel);
107+
getNodeData({ id: nodeLabel, type: nodeType });
105108
setShowFlyout(false);
106109
}}
107110
iconType="branch"

x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = {
8080
{
8181
selector: 'node',
8282
style: {
83-
'background-color': theme.euiColorGhost,
83+
'background-color': (el: cytoscape.NodeSingular) =>
84+
el.data('isRoot') ? theme.euiColorLightShade : theme.euiColorGhost,
8485
'background-height': '60%',
8586
'background-width': '60%',
8687
'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el),

x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import React, { FC } from 'react';
88
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
9+
import { FormattedMessage } from '@kbn/i18n/react';
910
import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics';
1011

1112
export const JobMapLegend: FC = () => (
@@ -17,7 +18,10 @@ export const JobMapLegend: FC = () => (
1718
</EuiFlexItem>
1819
<EuiFlexItem grow={false}>
1920
<EuiText size="xs" color="subdued">
20-
{JOB_MAP_NODE_TYPES.INDEX}
21+
<FormattedMessage
22+
id="xpack.ml.dataframe.analyticsMap.legend.indexLabel"
23+
defaultMessage="index"
24+
/>
2125
</EuiText>
2226
</EuiFlexItem>
2327
</EuiFlexGroup>
@@ -41,19 +45,40 @@ export const JobMapLegend: FC = () => (
4145
</EuiFlexItem>
4246
<EuiFlexItem grow={false}>
4347
<EuiText size="xs" color="subdued">
44-
{JOB_MAP_NODE_TYPES.ANALYTICS}
48+
<FormattedMessage
49+
id="xpack.ml.dataframe.analyticsMap.legend.analyticsJobLabel"
50+
defaultMessage="analytics job"
51+
/>
4552
</EuiText>
4653
</EuiFlexItem>
4754
</EuiFlexGroup>
4855
</EuiFlexItem>
4956
<EuiFlexItem grow={false}>
5057
<EuiFlexGroup gutterSize="xs" alignItems="center">
5158
<EuiFlexItem grow={false}>
52-
<span className="mlJobMapLegend__inferenceModel" />
59+
<span className="mlJobMapLegend__trainedModel" />
5360
</EuiFlexItem>
5461
<EuiFlexItem grow={false}>
5562
<EuiText size="xs" color="subdued">
56-
{'inference model'}
63+
<FormattedMessage
64+
id="xpack.ml.dataframe.analyticsMap.legend.trainedModelLabel"
65+
defaultMessage="trained model"
66+
/>
67+
</EuiText>
68+
</EuiFlexItem>
69+
</EuiFlexGroup>
70+
</EuiFlexItem>
71+
<EuiFlexItem grow={false}>
72+
<EuiFlexGroup gutterSize="xs" alignItems="center">
73+
<EuiFlexItem grow={false}>
74+
<span className="mlJobMapLegend__sourceNode" />
75+
</EuiFlexItem>
76+
<EuiFlexItem grow={false}>
77+
<EuiText size="xs" color="subdued">
78+
<FormattedMessage
79+
id="xpack.ml.dataframe.analyticsMap.legend.rootNodeLabel"
80+
defaultMessage="source node"
81+
/>
5782
</EuiText>
5883
</EuiFlexItem>
5984
</EuiFlexGroup>

x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Cytoscape, Controls, JobMapLegend } from './components';
1515
import { ml } from '../../../services/ml_api_service';
1616
import { useMlKibana } from '../../../contexts/kibana';
1717
import { useRefDimensions } from './components/use_ref_dimensions';
18+
import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics';
1819

1920
const cytoscapeDivStyle = {
2021
background: `linear-gradient(
@@ -36,22 +37,36 @@ ${theme.euiColorLightShade}`,
3637
marginTop: 0,
3738
};
3839

39-
export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => (
40+
export const JobMapTitle: React.FC<{ analyticsId?: string; modelId?: string }> = ({
41+
analyticsId,
42+
modelId,
43+
}) => (
4044
<EuiTitle size="xs">
4145
<span>
42-
{i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', {
43-
defaultMessage: 'Map for analytics ID {analyticsId}',
44-
values: { analyticsId },
45-
})}
46+
{analyticsId
47+
? i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', {
48+
defaultMessage: 'Map for analytics ID {analyticsId}',
49+
values: { analyticsId },
50+
})
51+
: i18n.translate('xpack.ml.dataframe.analyticsMap.modelIdTitle', {
52+
defaultMessage: 'Map for trained model ID {modelId}',
53+
values: { modelId },
54+
})}
4655
</span>
4756
</EuiTitle>
4857
);
4958

59+
interface GetDataObjectParameter {
60+
id: string;
61+
type: string;
62+
}
63+
5064
interface Props {
51-
analyticsId: string;
65+
analyticsId?: string;
66+
modelId?: string;
5267
}
5368

54-
export const JobMap: FC<Props> = ({ analyticsId }) => {
69+
export const JobMap: FC<Props> = ({ analyticsId, modelId }) => {
5570
const [elements, setElements] = useState<cytoscape.ElementDefinition[]>([]);
5671
const [nodeDetails, setNodeDetails] = useState({});
5772
const [error, setError] = useState(undefined);
@@ -60,14 +75,33 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
6075
services: { notifications },
6176
} = useMlKibana();
6277

63-
const getData = async (id?: string) => {
78+
const getDataWrapper = async (params?: GetDataObjectParameter) => {
79+
const { id, type } = params ?? {};
6480
const treatAsRoot = id !== undefined;
65-
const idToUse = treatAsRoot ? id : analyticsId;
66-
// Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it
81+
let idToUse: string;
82+
83+
if (id !== undefined) {
84+
idToUse = id;
85+
} else if (modelId !== undefined) {
86+
idToUse = modelId;
87+
} else {
88+
idToUse = analyticsId as string;
89+
}
90+
91+
await getData(
92+
idToUse,
93+
treatAsRoot,
94+
modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type
95+
);
96+
};
97+
98+
const getData = async (idToUse: string, treatAsRoot: boolean, type?: string) => {
99+
// Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it
67100
// TODO: update analyticsMap return type here
68101
const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap(
69102
idToUse,
70-
treatAsRoot
103+
treatAsRoot,
104+
type
71105
);
72106

73107
const { elements: nodeElements, details, error: fetchError } = analyticsMap;
@@ -86,7 +120,7 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
86120
}
87121

88122
if (nodeElements && nodeElements.length > 0) {
89-
if (id === undefined) {
123+
if (treatAsRoot === false) {
90124
setElements(nodeElements);
91125
setNodeDetails(details);
92126
} else {
@@ -98,8 +132,8 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
98132
};
99133

100134
useEffect(() => {
101-
getData();
102-
}, [analyticsId]);
135+
getDataWrapper();
136+
}, [analyticsId, modelId]);
103137

104138
if (error !== undefined) {
105139
notifications.toasts.addDanger(
@@ -119,14 +153,19 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
119153
<div style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) }} ref={ref}>
120154
<EuiFlexGroup justifyContent="spaceBetween">
121155
<EuiFlexItem grow={false}>
122-
<JobMapTitle analyticsId={analyticsId} />
156+
<JobMapTitle analyticsId={analyticsId} modelId={modelId} />
123157
</EuiFlexItem>
124158
<EuiFlexItem grow={false}>
125159
<JobMapLegend />
126160
</EuiFlexItem>
127161
</EuiFlexGroup>
128162
<Cytoscape height={height} elements={elements} width={width} style={cytoscapeDivStyle}>
129-
<Controls details={nodeDetails} getNodeData={getData} analyticsId={analyticsId} />
163+
<Controls
164+
details={nodeDetails}
165+
getNodeData={getDataWrapper}
166+
analyticsId={analyticsId}
167+
modelId={modelId}
168+
/>
130169
</Cytoscape>
131170
</div>
132171
</>

0 commit comments

Comments
 (0)