Skip to content

Commit

Permalink
[ML] Data frame analytics: Adds functionality to map view (elastic#83710
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
alvarezmelissa87 authored Nov 20, 2020
1 parent 22e494e commit 00e5951
Show file tree
Hide file tree
Showing 17 changed files with 564 additions and 281 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/constants/data_frame_analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const JOB_MAP_NODE_TYPES = {
ANALYTICS: 'analytics',
TRANSFORM: 'transform',
INDEX: 'index',
INFERENCE_MODEL: 'inferenceModel',
TRAINED_MODEL: 'trainedModel',
} as const;

export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES];
3 changes: 3 additions & 0 deletions x-pack/plugins/ml/common/types/ml_url_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export type TimeSeriesExplorerUrlState = MLPageState<

export interface DataFrameAnalyticsQueryState {
jobId?: JobId | JobId[];
modelId?: string;
groupIds?: string[];
globalState?: MlCommonGlobalState;
}
Expand All @@ -170,6 +171,7 @@ export interface DataFrameAnalyticsExplorationQueryState {
jobId: JobId;
analysisType: DataFrameAnalysisConfigType;
defaultIsTraining?: boolean;
modelId?: string;
};
}

Expand All @@ -180,6 +182,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState<
analysisType: DataFrameAnalysisConfigType;
globalState?: MlCommonGlobalState;
defaultIsTraining?: boolean;
modelId?: string;
}
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ interface Tab {
path: string;
}

export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({
jobId,
selectedTabId,
}) => {
export const AnalyticsNavigationBar: FC<{
selectedTabId?: string;
jobId?: string;
modelId?: string;
}> = ({ jobId, modelId, selectedTabId }) => {
const navigateToPath = useNavigateToPath();

const tabs = useMemo(() => {
Expand All @@ -38,7 +39,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string
path: '/data_frame_analytics/models',
},
];
if (jobId !== undefined) {
if (jobId !== undefined || modelId !== undefined) {
navTabs.push({
id: 'map',
name: i18n.translate('xpack.ml.dataframe.mapTabLabel', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ export const ModelsList: FC = () => {
onClick: async (item) => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP,
pageState: { jobId: item.metadata?.analytics_config.id },
pageState: { modelId: item.model_id },
});

await navigateToPath(path, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const Page: FC = () => {
const location = useLocation();
const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]);
const mapJobId = globalState?.ml?.jobId;
const mapModelId = globalState?.ml?.modelId;

return (
<Fragment>
Expand Down Expand Up @@ -106,8 +107,14 @@ export const Page: FC = () => {
<UpgradeWarning />

<EuiPageContent>
<AnalyticsNavigationBar selectedTabId={selectedTabId} jobId={mapJobId} />
{selectedTabId === 'map' && mapJobId && <JobMap analyticsId={mapJobId} />}
<AnalyticsNavigationBar
selectedTabId={selectedTabId}
jobId={mapJobId}
modelId={mapModelId}
/>
{selectedTabId === 'map' && (mapJobId || mapModelId) && (
<JobMap analyticsId={mapJobId} modelId={mapModelId} />
)}
{selectedTabId === 'data_frame_analytics' && (
<DataFrameAnalyticsList
blockRefresh={blockRefresh}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
.mlJobMapLegend__indexPattern {
height: $euiSizeM;
width: $euiSizeM;
background-color: '#FFFFFF';
background-color: $euiColorGhost;
border: 1px solid $euiColorVis2;
transform: rotate(45deg);
display: 'inline-block';
Expand All @@ -14,25 +14,34 @@
.mlJobMapLegend__transform {
height: $euiSizeM;
width: $euiSizeM;
background-color: '#FFFFFF';
background-color: $euiColorGhost;
border: 1px solid $euiColorVis1;
display: 'inline-block';
}

.mlJobMapLegend__analytics {
height: $euiSizeM;
width: $euiSizeM;
background-color: '#FFFFFF';
background-color: $euiColorGhost;
border: 1px solid $euiColorVis0;
border-radius: 50%;
border-radius: $euiBorderRadius;
display: 'inline-block';
}

.mlJobMapLegend__inferenceModel {
.mlJobMapLegend__trainedModel {
height: $euiSizeM;
width: $euiSizeM;
background-color: '#FFFFFF';
border: 1px solid $euiColorMediumShade;
border-radius: 50%;
background-color: $euiColorGhost;
border: $euiBorderThin;
border-radius: $euiBorderRadius;
display: 'inline-block';
}

.mlJobMapLegend__sourceNode {
height: $euiSizeM;
width: $euiSizeM;
background-color: $euiColorLightShade;
border: $euiBorderThin;
border-radius: $euiBorderRadius;
display: 'inline-block';
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description
import { CytoscapeContext } from './cytoscape';
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/util/date_utils';
import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics';
// import { DeleteButton } from './delete_button';
// import { DeleteButton } from './delete_button'; // TODO: add delete functionality in followup

interface Props {
analyticsId: string;
analyticsId?: string;
modelId?: string;
details: any;
getNodeData: any;
}
Expand Down Expand Up @@ -56,7 +57,7 @@ function getListItems(details: object): EuiDescriptionListProps['listItems'] {
});
}

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

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

const nodeDataButton =
analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? (
analyticsId !== nodeLabel &&
modelId !== nodeLabel &&
(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX) ? (
<EuiButtonEmpty
onClick={() => {
getNodeData(nodeLabel);
getNodeData({ id: nodeLabel, type: nodeType });
setShowFlyout(false);
}}
iconType="branch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = {
{
selector: 'node',
style: {
'background-color': theme.euiColorGhost,
'background-color': (el: cytoscape.NodeSingular) =>
el.data('isRoot') ? theme.euiColorLightShade : theme.euiColorGhost,
'background-height': '60%',
'background-width': '60%',
'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

export const JobMapLegend: FC = () => (
Expand All @@ -17,7 +18,10 @@ export const JobMapLegend: FC = () => (
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{JOB_MAP_NODE_TYPES.INDEX}
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.legend.indexLabel"
defaultMessage="index"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -41,19 +45,40 @@ export const JobMapLegend: FC = () => (
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{JOB_MAP_NODE_TYPES.ANALYTICS}
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.legend.analyticsJobLabel"
defaultMessage="analytics job"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<span className="mlJobMapLegend__inferenceModel" />
<span className="mlJobMapLegend__trainedModel" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{'inference model'}
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.legend.trainedModelLabel"
defaultMessage="trained model"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<span className="mlJobMapLegend__sourceNode" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.ml.dataframe.analyticsMap.legend.rootNodeLabel"
defaultMessage="source node"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Cytoscape, Controls, JobMapLegend } from './components';
import { ml } from '../../../services/ml_api_service';
import { useMlKibana } from '../../../contexts/kibana';
import { useRefDimensions } from './components/use_ref_dimensions';
import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics';

const cytoscapeDivStyle = {
background: `linear-gradient(
Expand All @@ -36,22 +37,36 @@ ${theme.euiColorLightShade}`,
marginTop: 0,
};

export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => (
export const JobMapTitle: React.FC<{ analyticsId?: string; modelId?: string }> = ({
analyticsId,
modelId,
}) => (
<EuiTitle size="xs">
<span>
{i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', {
defaultMessage: 'Map for analytics ID {analyticsId}',
values: { analyticsId },
})}
{analyticsId
? i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', {
defaultMessage: 'Map for analytics ID {analyticsId}',
values: { analyticsId },
})
: i18n.translate('xpack.ml.dataframe.analyticsMap.modelIdTitle', {
defaultMessage: 'Map for trained model ID {modelId}',
values: { modelId },
})}
</span>
</EuiTitle>
);

interface GetDataObjectParameter {
id: string;
type: string;
}

interface Props {
analyticsId: string;
analyticsId?: string;
modelId?: string;
}

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

const getData = async (id?: string) => {
const getDataWrapper = async (params?: GetDataObjectParameter) => {
const { id, type } = params ?? {};
const treatAsRoot = id !== undefined;
const idToUse = treatAsRoot ? id : analyticsId;
// Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it
let idToUse: string;

if (id !== undefined) {
idToUse = id;
} else if (modelId !== undefined) {
idToUse = modelId;
} else {
idToUse = analyticsId as string;
}

await getData(
idToUse,
treatAsRoot,
modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type
);
};

const getData = async (idToUse: string, treatAsRoot: boolean, type?: string) => {
// Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it
// TODO: update analyticsMap return type here
const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap(
idToUse,
treatAsRoot
treatAsRoot,
type
);

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

if (nodeElements && nodeElements.length > 0) {
if (id === undefined) {
if (treatAsRoot === false) {
setElements(nodeElements);
setNodeDetails(details);
} else {
Expand All @@ -98,8 +132,8 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
};

useEffect(() => {
getData();
}, [analyticsId]);
getDataWrapper();
}, [analyticsId, modelId]);

if (error !== undefined) {
notifications.toasts.addDanger(
Expand All @@ -119,14 +153,19 @@ export const JobMap: FC<Props> = ({ analyticsId }) => {
<div style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) }} ref={ref}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<JobMapTitle analyticsId={analyticsId} />
<JobMapTitle analyticsId={analyticsId} modelId={modelId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<JobMapLegend />
</EuiFlexItem>
</EuiFlexGroup>
<Cytoscape height={height} elements={elements} width={width} style={cytoscapeDivStyle}>
<Controls details={nodeDetails} getNodeData={getData} analyticsId={analyticsId} />
<Controls
details={nodeDetails}
getNodeData={getDataWrapper}
analyticsId={analyticsId}
modelId={modelId}
/>
</Cytoscape>
</div>
</>
Expand Down
Loading

0 comments on commit 00e5951

Please sign in to comment.