From c8106fd2005f35ee1a053d828de18cf996de883b Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 19 Oct 2020 14:05:42 -0600 Subject: [PATCH 01/14] add analytics map endpoint and server model --- .../ml_api_service/data_frame_analytics.ts | 8 + .../data_frame_analytics/analytics_manager.ts | 320 ++++++++++++++++++ .../models/data_frame_analytics/index.ts | 1 + .../ml/server/routes/data_frame_analytics.ts | 47 +++ .../routes/schemas/data_analytics_schema.ts | 5 + 5 files changed, 381 insertions(+) create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 7de39d91047ef..21556a4702b4e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -83,6 +83,14 @@ export const dataFrameAnalytics = { body, }); }, + getDataFrameAnalyticsMap(analyticsId?: string, treatAsRoot?: boolean) { + const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; + return http({ + path: `${basePath()}/data_frame/analytics/map${analyticsIdString}`, + method: 'GET', + query: { treatAsRoot }, + }); + }, evaluateDataFrameAnalytics(evaluateConfig: any) { const body = JSON.stringify(evaluateConfig); return http({ diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts new file mode 100644 index 0000000000000..1664f117ac087 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -0,0 +1,320 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { IScopedClusterClient } from 'kibana/server'; +import { JOB_MAP_NODE_TYPES } from '../../../public/application/data_frame_analytics/pages/job_map/common'; // eslint-disable-line +import { getAnalysisType } from '../../../public/application/data_frame_analytics/common/analytics'; // eslint-disable-line + +// interface NextLinkReturnType { +// isIndexPattern?: boolean; +// indexData?: any; +// isJob?: boolean; +// jobData?: any; +// isTransform?: boolean; +// transformData?: any; +// } +// interface AnalyticsMapReturnType { +// elements: any[]; +// details: object; // transform, job, or index details +// error: null | any; +// } +// interface AnalyticsMapElement { +// id: string; +// label: string; +// type: string; +// analysisType?: string; (job types type) +// } + +export class AnalyticsManager { + private _client: IScopedClusterClient['asInternalUser']; + + constructor(client: IScopedClusterClient['asInternalUser']) { + this._client = client; + } + + private isDuplicateElement(analyticsId: string, elements: any[]): boolean { + let isDuplicate = false; + elements.forEach((elem: any) => { + if (elem.data.label === analyticsId && elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS) { + isDuplicate = true; + } + }); + return isDuplicate; + } + + private async getAnalyticsModelData(modelId: string) { + const resp = await this._client.ml.getTrainedModels({ + model_id: modelId, + }); + const modelData = resp?.body?.trained_model_configs[0]; + return modelData; + } + + private async getAnalyticsJobData(analyticsId: string) { + const resp = await this._client.ml.getDataFrameAnalytics({ + id: analyticsId, + }); + const jobData = resp?.body?.data_frame_analytics[0]; + return jobData; + } + + private async getIndexData(index: string) { + const indexData = await this._client.indices.get({ + index, + }); + + return indexData?.body; + } + + private async getTransformData(transformId: string) { + const transform = await this._client.transform.getTransform({ + transform_id: transformId, + }); + const transformData = transform?.body?.transforms[0]; + return transformData; + } + + private async getNextLink({ id, type }: { id: string; type: JOB_MAP_NODE_TYPES }) { + try { + if (type === JOB_MAP_NODE_TYPES.INDEX_PATTERN) { + // fetch index data + const indexData = await this.getIndexData(id); + const meta = indexData[id].mappings._meta; + return { isIndexPattern: true, indexData, meta }; + } else if (type.includes(JOB_MAP_NODE_TYPES.ANALYTICS)) { + // fetch job associated with this index + const jobData = await this.getAnalyticsJobData(id); + return { jobData, isJob: true }; + } else if (type === JOB_MAP_NODE_TYPES.TRANSFORM) { + // fetch transform so we can get original index pattern + const transformData = await this.getTransformData(id); + return { transformData, isTransform: true }; + } + } catch (error) { + throw Boom.badData(error.message ? error.message : error); + } + } + + /** + * Works backward from jobId to return related jobs from source indices + * @param jobId + */ + async getAnalyticsMap(analyticsId: string) { + const result: any = { elements: [], details: {}, error: null }; + + try { + let data = await this.getAnalyticsJobData(analyticsId); + let nextLinkId = data?.source?.index[0]; + let nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + let complete = false; + let link: any = {}; + let count = 0; + let rootTransform; + let rootIndexPattern; + + const firstNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + result.elements.push({ + data: { + id: firstNodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + }, + }); + result.details[firstNodeId] = data; + // Add a safeguard against infinite loops. + while (complete === false) { + count++; + if (count >= 50) { + break; + } + + try { + link = await this.getNextLink({ + id: nextLinkId, + type: nextType, + }); + } catch (error) { + result.error = error.message || 'Something went wrong'; + break; + } + // If it's index pattern, check meta data to see what to fetch next + if (link.isIndexPattern === true) { + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + result.elements.unshift({ + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX_PATTERN }, + }); + result.details[nodeId] = link.indexData; + + if (link.meta?.created_by === 'data-frame-analytics') { + nextLinkId = link.meta.analytics; + nextType = JOB_MAP_NODE_TYPES.ANALYTICS; + } + + if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { + nextLinkId = link.meta._transform?.transform; + nextType = JOB_MAP_NODE_TYPES.TRANSFORM; + } + + // Check meta data + if (link.meta === undefined) { + rootIndexPattern = nextLinkId; + complete = true; + break; + } + } else if (link.isJob === true) { + data = link.jobData; + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + result.elements.unshift({ + data: { + id: nodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + } else if (link.isTransform === true) { + data = link.transformData; + + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; + rootTransform = data.dest.index; + + result.elements.unshift({ + data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + } + } // end while + + // create edge elements + const elemLength = result.elements.length - 1; + for (let i = 0; i < elemLength; i++) { + const currentElem = result.elements[i]; + const nextElem = result.elements[i + 1]; + result.elements.push({ + data: { + id: `${currentElem.data.id}~${nextElem.data.id}`, + source: currentElem.data.id, + target: nextElem.data.id, + }, + }); + } + + // fetch all jobs associated with root transform if defined, otherwise check root index + if (rootTransform !== undefined || rootIndexPattern !== undefined) { + const analyticsJobs = await this._client.ml.getDataFrameAnalytics(); + const jobs = analyticsJobs?.body?.data_frame_analytics || []; + const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === comparator && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + result.elements.push({ + data: { + id: `${source}~${nodeId}`, + source, + target: nodeId, + }, + }); + } + } + } + + return result; + } catch (error) { + result.error = error.message || 'An error occurred fetching map'; + return result; + } + } + + async extendAnalyticsMapForAnalyticsJob(analyticsId: string) { + const result: any = { elements: [], details: {}, error: null }; + + try { + const jobData = await this.getAnalyticsJobData(analyticsId); + const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + const destIndex = Array.isArray(jobData?.dest?.index) + ? jobData?.dest?.index[0] + : jobData?.dest?.index; + const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + const analyticsJobs = await this._client.ml.getDataFrameAnalytics(); + const jobs = analyticsJobs?.body?.data_frame_analytics || []; + + // If destIndex node has not been created, create it + const destIndexDetails = await this.getIndexData(destIndex); + result.elements.push({ + data: { + id: destIndexNodeId, + label: destIndex, + type: JOB_MAP_NODE_TYPES.INDEX_PATTERN, + }, + }); + result.details[destIndexNodeId] = destIndexDetails; + + // Connect incoming job to destIndex + result.elements.push({ + data: { + id: `${currentJobNodeId}~${destIndexNodeId}`, + source: currentJobNodeId, + target: destIndexNodeId, + }, + }); + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === destIndex && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + // Create node for associated job + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + + result.elements.push({ + data: { + id: `${destIndexNodeId}~${nodeId}`, + source: destIndexNodeId, + target: nodeId, + }, + }); + } + } + } catch (error) { + result.error = error.message || 'An error occurred fetching map'; + return result; + } + + return result; + } +} diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts index fc18436ff5216..95c5d2848ae7a 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts @@ -6,3 +6,4 @@ export { analyticsAuditMessagesProvider } from './analytics_audit_messages'; export { modelsProvider } from './models_provider'; +export { AnalyticsManager } from './analytics_manager'; diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 4364202a5c9af..232844babf406 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -14,11 +14,13 @@ import { dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, analyticsIdSchema, + analyticsMapQuerySchema, stopsDataFrameAnalyticsJobQuerySchema, deleteDataFrameAnalyticsJobSchema, jobsExistSchema, } from './schemas/data_analytics_schema'; import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns'; +import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manager'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; import { getAuthorizationHeader } from '../lib/request_authorization'; import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; @@ -33,6 +35,16 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern return iph.deleteIndexPatternById(indexPatternId); } +function getAnalyticsMap(client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(client.asInternalUser); + return analytics.getAnalyticsMap(analyticsId); +} + +function getExtendedMap(client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(client.asInternalUser); + return analytics.extendAnalyticsMapForAnalyticsJob(analyticsId); +} + /** * Routes for the data frame analytics */ @@ -598,4 +610,39 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout } }) ); + + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/map/:analyticsId Get objects leading up to analytics job + * @apiName GetDataFrameAnalyticsIdMap + * @apiDescription Returns map of objects leading up to analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ + router.get( + { + path: '/api/ml/data_frame/analytics/map/{analyticsId}', + validate: { + params: analyticsIdSchema, + query: analyticsMapQuerySchema, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, client, request, response }) => { + try { + const { analyticsId } = request.params; + const treatAsRoot = request.query?.treatAsRoot; + const caller = + treatAsRoot === 'true' || treatAsRoot === true ? getExtendedMap : getAnalyticsMap; + const results = await caller(mlClient, client, analyticsId); + + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} } diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 846f79fbe0d8a..d8226b70eb2c3 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -38,6 +38,7 @@ export const dataAnalyticsEvaluateSchema = schema.object({ schema.object({ regression: schema.maybe(schema.any()), classification: schema.maybe(schema.any()), + outlier_detection: schema.maybe(schema.any()), }) ), }); @@ -86,3 +87,7 @@ export const jobsExistSchema = schema.object({ analyticsIds: schema.arrayOf(schema.string()), allSpaces: schema.maybe(schema.boolean()), }); + +export const analyticsMapQuerySchema = schema.maybe( + schema.object({ treatAsRoot: schema.maybe(schema.any()) }) +); From 2533c79ff2356c5d73a403b15de6836b10aecb3f Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 19 Oct 2020 14:09:09 -0600 Subject: [PATCH 02/14] add map action to job and models list --- .../data_frame_analytics/_index.scss | 1 + .../data_frame_analytics/common/analytics.ts | 2 +- .../components/action_map/index.ts | 8 + .../components/action_map/map_button.tsx | 50 ++++++ .../components/action_map/use_map_action.tsx | 41 +++++ .../components/analytics_list/common.ts | 4 + .../components/analytics_list/use_actions.tsx | 3 + .../analytics_navigation_bar.tsx | 19 ++- .../models_management/models_list.tsx | 17 ++ .../pages/analytics_management/page.tsx | 7 +- .../pages/job_map/common.ts | 11 ++ .../pages/job_map/components/_index.scss | 1 + .../pages/job_map/components/_legend.scss | 26 +++ .../pages/job_map/components/controls.tsx | 151 ++++++++++++++++++ .../pages/job_map/components/cytoscape.tsx | 122 ++++++++++++++ .../job_map/components/cytoscape_options.tsx | 100 ++++++++++++ .../job_map/components/delete_button.tsx | 55 +++++++ .../pages/job_map/components/index.ts | 9 ++ .../pages/job_map/components/legend.tsx | 50 ++++++ .../job_map/components/use_ref_dimensions.ts | 21 +++ .../pages/job_map/index.ts | 7 + .../pages/job_map/job_map.tsx | 130 +++++++++++++++ .../data_frame_analytics/analytics_map.tsx | 60 +++++++ .../routes/data_frame_analytics/index.ts | 1 + 24 files changed, 888 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx create mode 100644 x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss index 231d0f6a0d8c5..a043a691c9ef6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -1,4 +1,5 @@ @import 'pages/analytics_exploration/components/regression_exploration/index'; +@import 'pages/job_map/components/index'; @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_button/index'; @import 'pages/analytics_creation/components/index'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 6e42e3e2f51fa..4f13b9a526323 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -162,7 +162,7 @@ interface LoadEvaluateResult { export const getAnalysisType = ( analysis: AnalysisConfig ): DataFrameAnalysisConfigType | 'unknown' => { - const keys = Object.keys(analysis); + const keys = Object.keys(analysis || {}); if (keys.length === 1) { return keys[0] as DataFrameAnalysisConfigType; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts new file mode 100644 index 0000000000000..4004f019c732b --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MapButton } from './map_button'; +export { useMapAction } from './use_map_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx new file mode 100644 index 0000000000000..4015d03cc6dcc --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; + +import { + isRegressionAnalysis, + isOutlierAnalysis, + isClassificationAnalysis, +} from '../../../../common/analytics'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export const mapActionButtonText = i18n.translate( + 'xpack.ml.dataframe.analyticsList.mapActionName', + { + defaultMessage: 'Map', + } +); +interface MapButtonProps { + item: DataFrameAnalyticsListRow; +} + +export const MapButton: FC = ({ item }) => { + const toolTipContent = i18n.translate( + 'xpack.ml.dataframe.analyticsList.mapActionDisabledTooltipContent', + { + defaultMessage: 'Unknown analysis type.', + } + ); + const disabled = + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); + + if (disabled) { + return ( + + <>{mapActionButtonText} + + ); + } + + return <>{mapActionButtonText}; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx new file mode 100644 index 0000000000000..5e88b9a5c78b0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; + +import { + getJobMapUrl, + DataFrameAnalyticsListAction, + DataFrameAnalyticsListRow, +} from '../analytics_list/common'; + +import { mapActionButtonText, MapButton } from './map_button'; + +export type MapAction = ReturnType; +export const useMapAction = () => { + const navigateToPath = useNavigateToPath(); + + const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => { + navigateToPath(getJobMapUrl(item.id)); + }, []); + + const action: DataFrameAnalyticsListAction = useMemo( + () => ({ + isPrimary: true, + name: (item: DataFrameAnalyticsListRow) => , + enabled: () => true, + description: mapActionButtonText, + icon: 'graphApp', + type: 'icon', + onClick: clickHandler, + 'data-test-subj': 'mlAnalyticsJobMapButton', + }), + [clickHandler] + ); + + return { action }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 8c7c8b9db8b64..d5b75c4c7659b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -132,6 +132,10 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) { return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100; } +export function getJobMapUrl(jobId: string) { + return `#/data_frame_analytics/map?_g=(ml:(jobId:${jobId}))`; +} + // The single Action type is not exported as is // from EUI so we use that code to get the single // Action type from the array of actions. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index 20ae48a12ecf9..74b367cc7ab13 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -16,6 +16,7 @@ import { isEditActionFlyoutVisible, useEditAction, EditActionFlyout } from '../a import { useStartAction, StartActionModal } from '../action_start'; import { useStopAction, StopActionModal } from '../action_stop'; import { useViewAction } from '../action_view'; +import { useMapAction } from '../action_map'; import { DataFrameAnalyticsListRow } from './common'; @@ -30,6 +31,7 @@ export const useActions = ( const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); const viewAction = useViewAction(); + const mapAction = useMapAction(); const cloneAction = useCloneAction(canCreateDataFrameAnalytics); const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics); const editAction = useEditAction(canStartStopDataFrameAnalytics); @@ -40,6 +42,7 @@ export const useActions = ( const actions: EuiTableActionsColumnType['actions'] = [ viewAction.action, + mapAction.action, ]; // isManagementTable will be the same for the lifecycle of the component diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index bd59749517052..a51cb0f7107a3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -18,8 +18,8 @@ interface Tab { export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selectedTabId }) => { const navigateToPath = useNavigateToPath(); - const tabs = useMemo( - () => [ + const tabs = useMemo(() => { + const navTabs = [ { id: 'data_frame_analytics', name: i18n.translate('xpack.ml.dataframe.jobsTabLabel', { @@ -34,9 +34,18 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selecte }), path: '/data_frame_analytics/models', }, - ], - [] - ); + ]; + if (selectedTabId === 'map') { + navTabs.push({ + id: 'map', + name: i18n.translate('xpack.ml.dataframe.mapTabLabel', { + defaultMessage: 'Map', + }), + path: '/data_frame_analytics/map', + }); + } + return navTabs; + }, [selectedTabId === 'map']); const onTabClick = useCallback(async (tab: Tab) => { await navigateToPath(tab.path); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 713cbbc814c32..86691d45ef44f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -30,6 +30,7 @@ import { ModelsTableToConfigMapping } from './index'; import { DeleteModelsModal } from './delete_models_modal'; import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana'; import { ExpandedRow } from './expanded_row'; +import { getJobMapUrl } from '../analytics_list/common'; import { TrainedModelConfigResponse, ModelPipelines, @@ -298,6 +299,22 @@ export const ModelsList: FC = () => { }, isPrimary: true, }, + { + name: i18n.translate('xpack.ml.inference.modelsList.analyticsMapActionLabel', { + defaultMessage: 'Analytics map', + }), + description: i18n.translate('xpack.ml.inference.modelsList.analyticsMapActionLabel', { + defaultMessage: 'Analytics map', + }), + icon: 'graphApp', + type: 'icon', + isPrimary: true, + available: (item) => item.metadata?.analytics_config?.id, + onClick: async (item) => { + // TODO: update to use new navigation? + await navigateToUrl(getJobMapUrl(item.metadata?.analytics_config.id)); + }, + }, { name: i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { defaultMessage: 'Delete model', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 7ffd477039e78..0a24c04ea3f53 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -31,8 +31,11 @@ import { NodeAvailableWarning } from '../../../components/node_available_warning import { UpgradeWarning } from '../../../components/upgrade'; import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; +import { JobMap } from '../job_map'; -export const Page: FC = () => { +export const Page: FC<{ + jobId?: string; +}> = ({ jobId }) => { const [blockRefresh, setBlockRefresh] = useState(false); useRefreshInterval(setBlockRefresh); @@ -88,7 +91,7 @@ export const Page: FC = () => { - + {selectedTabId === 'map' && jobId && } {selectedTabId === 'data_frame_analytics' && ( )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts new file mode 100644 index 0000000000000..bec40b726c432 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum JOB_MAP_NODE_TYPES { + ANALYTICS = 'analytics', + TRANSFORM = 'transform', + INDEX_PATTERN = 'index-pattern', +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss new file mode 100644 index 0000000000000..2bcc91f34d382 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss @@ -0,0 +1 @@ +@import 'legend'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss new file mode 100644 index 0000000000000..4a027f822856c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss @@ -0,0 +1,26 @@ +.mlJobMapLegend__container { + background-color: $euiColorLightestShade; +} + +.mlJobMapLegend__indexPattern { + height: $euiSizeM; + width: $euiSizeM; + background-color: $euiColorVis2; + transform: rotate(45deg); + display: 'inline-block'; +} + +.mlJobMapLegend__transform { + height: $euiSizeM; + width: $euiSizeM; + background-color: $euiColorVis1; + display: 'inline-block'; +} + +.mlJobMapLegend__analytics { + height: $euiSizeM; + width: $euiSizeM; + background-color: $euiColorVis0; + border-radius: 50%; + display: 'inline-block'; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx new file mode 100644 index 0000000000000..130ba21f18c47 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState, useContext, useCallback } from 'react'; +import cytoscape from 'cytoscape'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiPortal, + EuiTitle, +} from '@elastic/eui'; +import { CytoscapeContext } from './cytoscape'; +import { JOB_MAP_NODE_TYPES } from '../common'; +import { DeleteButton } from './delete_button'; + +interface Props { + analyticsId: string; + details: any; + getNodeData: any; +} + +// TODO move list items to shared place as we use them in the details bit of the wizard +export interface ListItems { + title: string; + description: string | JSX.Element; +} + +function getListItems(details: object): ListItems[] { + return Object.entries(details).map(([key, value]) => ({ + title: key, + description: typeof value === 'object' ? JSON.stringify(value, null, 2) : value, + })); +} + +export const Controls: FC = ({ analyticsId, details, getNodeData }) => { + const [showFlyout, setShowFlyout] = useState(false); + const [selectedNode, setSelectedNode] = useState(undefined); + + const cy = useContext(CytoscapeContext); + const deselect = useCallback(() => { + if (cy) { + cy.elements().unselect(); + } + setShowFlyout(false); + setSelectedNode(undefined); + }, [cy, setSelectedNode]); + + const nodeId = selectedNode?.data('id'); + const nodeLabel = selectedNode?.data('label'); + const nodeType = selectedNode?.data('type'); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = (event) => { + setSelectedNode(event.target); + setShowFlyout(true); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + // cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + // cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + if (showFlyout === false) { + return null; + } + + const nodeDataButton = + analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? ( + { + getNodeData(nodeLabel); + setShowFlyout(false); + }} + iconType="branch" + > + {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton', { + defaultMessage: 'Fetch related nodes', + })} + + ) : null; + + return ( + + setShowFlyout(false)} + data-test-subj="mlAnalyticsJobMapFlyout" + > + + + + +

+ {i18n.translate('xpack.ml.dataframe.analyticsMap.flyoutHeaderTitle', { + defaultMessage: 'Details for {type} {id}', + values: { id: nodeLabel, type: nodeType }, + })} +

+
+
+
+
+ + + + + + + + + + {nodeDataButton} + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx new file mode 100644 index 0000000000000..45e62c868547a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { + CSSProperties, + useState, + useRef, + useEffect, + ReactNode, + createContext, + useCallback, +} from 'react'; +import cytoscape from 'cytoscape'; +import { cytoscapeOptions } from './cytoscape_options'; + +export const CytoscapeContext = createContext(undefined); + +interface CytoscapeProps { + children?: ReactNode; + elements: cytoscape.ElementDefinition[]; + height: number; + style?: CSSProperties; + width: number; +} + +function useCytoscape(options: cytoscape.CytoscapeOptions) { + const [cy, setCy] = useState(undefined); + const ref = useRef(null); + + useEffect(() => { + if (!cy) { + setCy(cytoscape({ ...options, container: ref.current })); + } + }, [options, cy]); + + // Destroy the cytoscape instance on unmount + useEffect(() => { + return () => { + if (cy) { + cy.destroy(); + } + }; + }, [cy]); + + return [ref, cy] as [React.MutableRefObject, cytoscape.Core | undefined]; +} + +function getLayoutOptions(width: number, height: number) { + return { + name: 'breadthfirst', + directed: true, + fit: true, + spacingFactor: 0.85, + boundingBox: { x1: 0, y1: 0, w: width, h: height }, + }; +} + +export function Cytoscape({ children, elements, height, style, width }: CytoscapeProps) { + const [ref, cy] = useCytoscape({ + ...cytoscapeOptions, + elements, + }); + + // 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 = { ...style, height }; + + const dataHandler = useCallback( + (event) => { + if (cy) { + const layout = cy.layout(getLayoutOptions(width, height)); + layout.run(); + } + }, + [cy, height, width] + ); + + // Set up cytoscape event handlers + useEffect(() => { + const mouseoverHandler: cytoscape.EventHandler = (event) => { + event.target.addClass('hover'); + event.target.connectedEdges().addClass('nodeHover'); + }; + const mouseoutHandler: cytoscape.EventHandler = (event) => { + event.target.removeClass('hover'); + event.target.connectedEdges().removeClass('nodeHover'); + }; + + if (cy) { + cy.on('data', dataHandler); + cy.on('mouseover', 'edge, node', mouseoverHandler); + cy.on('mouseout', 'edge, node', mouseoutHandler); + } + + return () => { + if (cy) { + cy.removeListener('data', undefined, dataHandler as cytoscape.EventHandler); + cy.removeListener('mouseover', 'edge, node', mouseoverHandler); + cy.removeListener('mouseout', 'edge, node', mouseoutHandler); + } + }; + }, [cy, elements, height, width]); + + // Trigger a custom "data" event when data changes + useEffect(() => { + if (cy) { + cy.add(elements); + cy.trigger('data'); + } + }, [cy, elements]); + + return ( + +
+ {children} +
+
+ ); +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx new file mode 100644 index 0000000000000..d5a3a3f1689bf --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import cytoscape from 'cytoscape'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { JOB_MAP_NODE_TYPES } from '../common'; + +const lineColor = '#C5CCD7'; + +enum MAP_SHAPES { + ELLIPSE = 'ellipse', + RECTANGLE = 'rectangle', + DIAMOND = 'diamond', +} + +function shapeForNode(el: cytoscape.NodeSingular): MAP_SHAPES { + const type = el.data('type'); + switch (type) { + case JOB_MAP_NODE_TYPES.ANALYTICS: + return MAP_SHAPES.ELLIPSE; + case JOB_MAP_NODE_TYPES.TRANSFORM: + return MAP_SHAPES.RECTANGLE; + case JOB_MAP_NODE_TYPES.INDEX_PATTERN: + return MAP_SHAPES.DIAMOND; + default: + return MAP_SHAPES.ELLIPSE; + } +} + +function colorForNode(el: cytoscape.NodeSingular) { + const type = el.data('type'); + switch (type) { + case JOB_MAP_NODE_TYPES.ANALYTICS: + return theme.euiColorVis0; + case JOB_MAP_NODE_TYPES.TRANSFORM: + return theme.euiColorVis1; + case JOB_MAP_NODE_TYPES.INDEX_PATTERN: + return theme.euiColorVis2; + default: + return 'white'; + } +} + +export const cytoscapeOptions: cytoscape.CytoscapeOptions = { + autoungrabify: true, + boxSelectionEnabled: false, + maxZoom: 3, + minZoom: 0.2, + // @ts-ignore + style: [ + { + selector: 'node', + style: { + 'background-color': (el: cytoscape.NodeSingular) => colorForNode(el), + 'background-height': '60%', + 'background-width': '60%', + 'border-color': (el: cytoscape.NodeSingular) => + el.selected() ? theme.euiColorPrimary : theme.euiColorMediumShade, + 'border-width': 2, + // @ts-ignore + color: theme.textColors.default, + 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', + 'font-size': theme.euiFontSizeXS, + // @ts-ignore + 'min-zoomed-font-size': theme.euiSizeL, + label: 'data(label)', + shape: (el: cytoscape.NodeSingular) => shapeForNode(el), + 'text-background-color': theme.euiColorLightestShade, + 'text-background-opacity': 0, + // @ts-ignore + 'text-background-padding': theme.paddingSizes.xs, + 'text-background-shape': 'roundrectangle', + // @ts-ignore + 'text-margin-y': theme.paddingSizes.s, + 'text-max-width': '200px', + 'text-valign': 'bottom', + 'text-wrap': 'ellipsis', + }, + }, + { + selector: 'edge', + style: { + 'curve-style': 'taxi', + // @ts-ignore + 'taxi-direction': 'rightward', + 'line-color': lineColor, + 'overlay-opacity': 0, + 'target-arrow-color': lineColor, + 'target-arrow-shape': 'triangle', + // @ts-ignore + 'target-distance-from-node': theme.paddingSizes.xs, + width: 1, + 'source-arrow-shape': 'none', + }, + }, + ], +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx new file mode 100644 index 0000000000000..74ec6067a6349 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ml } from '../../../../services/ml_api_service'; +import { getToastNotifications } from '../../../../util/dependency_cache'; +import { JOB_MAP_NODE_TYPES } from '../common'; + +interface Props { + id: string; + type: JOB_MAP_NODE_TYPES; +} + +export const DeleteButton: FC = ({ id, type }) => { + const toastNotifications = getToastNotifications(); + + const onDelete = async () => { + try { + // if (isDataFrameAnalyticsFailed(d.stats.state)) { + // await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true); + // } + await ml.dataFrameAnalytics.deleteDataFrameAnalytics(id); + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsSuccessMessage', { + defaultMessage: 'Request to delete data frame analytics {id} acknowledged.', + values: { id }, + }) + ); + } catch (e) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsErrorMessage', { + defaultMessage: 'An error occurred deleting the data frame analytics {id}: {error}', + values: { id, error: JSON.stringify(e) }, + }) + ); + } + }; + + if (type !== JOB_MAP_NODE_TYPES.ANALYTICS) { + return null; + } + + return ( + + {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton', { + defaultMessage: 'Delete job', + })} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts new file mode 100644 index 0000000000000..7f99bb88ec0c8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Cytoscape, CytoscapeContext } from './cytoscape'; +export { Controls } from './controls'; +export { JobMapLegend } from './legend'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx new file mode 100644 index 0000000000000..69ad31b7ee48e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { JOB_MAP_NODE_TYPES } from '../common'; + +export const JobMapLegend: FC = () => ( + + + + + + + + + {JOB_MAP_NODE_TYPES.INDEX_PATTERN} + + + + + + + + + + + + {JOB_MAP_NODE_TYPES.TRANSFORM} + + + + + + + + + + + + {JOB_MAP_NODE_TYPES.ANALYTICS} + + + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts new file mode 100644 index 0000000000000..c8639b334f66a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useRef } from 'react'; +import { useWindowSize } from 'react-use'; + +export function useRefDimensions() { + const ref = useRef(null); + const windowHeight = useWindowSize().height; + + if (!ref.current) { + return { ref, width: 0, height: 0 }; + } + + const { top, width } = ref.current.getBoundingClientRect(); + const height = windowHeight - top; + + return { ref, width, height }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts new file mode 100644 index 0000000000000..59d94bb22980c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobMap } from './job_map'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx new file mode 100644 index 0000000000000..25a41c4eb16c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import cytoscape from 'cytoscape'; + +import { Cytoscape, Controls, JobMapLegend } from './components'; +import { ml } from '../../../services/ml_api_service'; +// TODO: don't use dep cache - switch to hook +import { getToastNotifications } from '../../../util/dependency_cache'; +import { useRefDimensions } from './components/use_ref_dimensions'; + +const cytoscapeDivStyle = { + background: `linear-gradient( + 90deg, + ${theme.euiPageBackgroundColor} + calc(${theme.euiSizeL} - calc(${theme.euiSizeXS} / 2)), + transparent 1% +) +center, +linear-gradient( + ${theme.euiPageBackgroundColor} + calc(${theme.euiSizeL} - calc(${theme.euiSizeXS} / 2)), + transparent 1% +) +center, +${theme.euiColorLightShade}`, + backgroundSize: `${theme.euiSizeL} ${theme.euiSizeL}`, + margin: `-${theme.gutterTypes.gutterLarge}`, + marginTop: 0, +}; + +export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => ( + + + {i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { + defaultMessage: 'Map for analytics ID {analyticsId}', + values: { analyticsId }, + })} + + +); + +interface Props { + analyticsId: string; +} + +export const JobMap: FC = ({ analyticsId }) => { + const toastNotifications = getToastNotifications(); + const [elements, setElements] = useState([]); + const [nodeDetails, setNodeDetails] = useState({}); + const [error, setError] = useState(undefined); + + const getData = async (id?: string) => { + const treatAsRoot = id !== undefined; + const idToUse = treatAsRoot ? id : analyticsId; + // Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it + // TODO: update analyticsMap return type here + const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap( + idToUse, + treatAsRoot + ); + // console.log('----- ANALYTICS MAP ----', JSON.stringify(analyticsMap, null, 2)); // remove + const { elements: nodeElements, details, error: fetchError } = analyticsMap; + + if (fetchError !== null) { + setError(fetchError); + } + + if (nodeElements && nodeElements.length === 0) { + toastNotifications.add( + i18n.translate('xpack.ml.dataframe.analyticsMap.emptyResponseMessage', { + defaultMessage: 'No related analytics jobs found for {id}.', + values: { id: idToUse }, + }) + ); + } + + if (nodeElements && nodeElements.length > 0) { + if (id === undefined) { + setElements(nodeElements); + setNodeDetails(details); + } else { + setElements([...elements, ...nodeElements]); + setNodeDetails({ ...details, ...nodeDetails }); + } + } + }; + + useEffect(() => { + getData(); + }, [analyticsId]); + + if (error !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsMap.fetchDataErrorMessage', { + defaultMessage: 'Unable to fetch some data. An error occurred: {error}', + values: { error: JSON.stringify(error) }, + }) + ); + setError(undefined); + } + + const { ref, width, height } = useRefDimensions(); + + return ( + <> + +
+ + + + + + + + + + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx new file mode 100644 index 0000000000000..0788de4de2913 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; +import { decode } from 'rison-node'; + +import { NavigateToPath } from '../../../contexts/kibana'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_management'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsMapRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + path: '/data_frame_analytics/map', + render: (props, deps) => , + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.analyticsMapLabel', { + defaultMessage: 'Analytics Map', + }), + href: '', + }, + ], +}); + +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { _g }: Record = parse(location.search, { sort: false }); + let jobId; + + if (_g !== undefined) { + let globalState: any = null; + try { + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global state'); + window.location.href = '#data_frame_analytics'; + } + jobId = globalState.ml?.jobId; + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts index c75a8240d28fb..eedcaaf41292b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -8,3 +8,4 @@ export * from './analytics_jobs_list'; export * from './analytics_job_exploration'; export * from './analytics_job_creation'; export * from './models_list'; +export * from './analytics_map'; From 616f02c2af32b134f96f9f4ece024fd1552252e9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 20 Oct 2020 17:10:23 -0600 Subject: [PATCH 03/14] wip:fetch models for jobs. Use url generator --- .../ml/common/constants/ml_url_generator.ts | 1 + .../components/action_map/use_map_action.tsx | 21 ++- .../analytics_navigation_bar.tsx | 11 +- .../pages/analytics_management/page.tsx | 2 +- .../pages/job_map/common.ts | 1 + .../pages/job_map/job_map.tsx | 2 +- .../data_frame_analytics_urls_generator.ts | 32 ++++ .../ml_url_generator/ml_url_generator.ts | 5 + .../data_frame_analytics/analytics_manager.ts | 174 ++++++++++++++---- .../models/data_frame_analytics/types.ts | 72 ++++++++ 10 files changed, 270 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/types.ts diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index 541b8af6fc0fc..a79e72a84c08e 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -12,6 +12,7 @@ export const ML_PAGES = { SINGLE_METRIC_VIEWER: 'timeseriesexplorer', DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', + DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map', /** * Page: Data Visualizer */ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx index 5e88b9a5c78b0..d4c02c9eb494d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -5,22 +5,25 @@ */ import React, { useCallback, useMemo } from 'react'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; - -import { - getJobMapUrl, - DataFrameAnalyticsListAction, - DataFrameAnalyticsListRow, -} from '../analytics_list/common'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana'; +import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; import { mapActionButtonText, MapButton } from './map_button'; export type MapAction = ReturnType; export const useMapAction = () => { + const mlUrlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); - const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => { - navigateToPath(getJobMapUrl(item.id)); + const clickHandler = useCallback(async (item: DataFrameAnalyticsListRow) => { + const path = await mlUrlGenerator.createUrl({ + // @ts-ignore + page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, + pageState: { jobId: item.id }, + }); + + await navigateToPath(path, false); }, []); const action: DataFrameAnalyticsListAction = useMemo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index a51cb0f7107a3..03c39caf69a8c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -15,7 +15,10 @@ interface Tab { path: string; } -export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selectedTabId }) => { +export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({ + jobId, + selectedTabId, +}) => { const navigateToPath = useNavigateToPath(); const tabs = useMemo(() => { @@ -35,7 +38,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selecte path: '/data_frame_analytics/models', }, ]; - if (selectedTabId === 'map') { + if (jobId !== undefined) { navTabs.push({ id: 'map', name: i18n.translate('xpack.ml.dataframe.mapTabLabel', { @@ -45,10 +48,10 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selecte }); } return navTabs; - }, [selectedTabId === 'map']); + }, []); const onTabClick = useCallback(async (tab: Tab) => { - await navigateToPath(tab.path); + await navigateToPath(tab.path, true); }, []); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 0a24c04ea3f53..964ca7d631273 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -90,7 +90,7 @@ export const Page: FC<{ - + {selectedTabId === 'map' && jobId && } {selectedTabId === 'data_frame_analytics' && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts index bec40b726c432..9e9d8179dcb16 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts @@ -8,4 +8,5 @@ export enum JOB_MAP_NODE_TYPES { ANALYTICS = 'analytics', TRANSFORM = 'transform', INDEX_PATTERN = 'index-pattern', + INFERENCE_MODEL = 'inferenceModel', } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 25a41c4eb16c1..2e17245805766 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -66,7 +66,7 @@ export const JobMap: FC = ({ analyticsId }) => { idToUse, treatAsRoot ); - // console.log('----- ANALYTICS MAP ----', JSON.stringify(analyticsMap, null, 2)); // remove + const { elements: nodeElements, details, error: fetchError } = analyticsMap; if (fetchError !== null) { diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 2408290e76773..6c58a9d28bcc2 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -82,3 +82,35 @@ export function createDataFrameAnalyticsExplorationUrl( return url; } + +/** + * Creates URL to the DataFrameAnalytics Map page + */ +export function createDataFrameAnalyticsMapUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsExplorationUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`; + + if (mlUrlGeneratorState) { + const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; + + const queryState: DataFrameAnalyticsExplorationQueryState = { + ml: { + jobId, + analysisType, + defaultIsTraining, + }, + ...globalState, + }; + + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts index 351e366d1f1d8..a3f1ed6f78e8d 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -23,6 +23,7 @@ import { import { createDataFrameAnalyticsJobManagementUrl, createDataFrameAnalyticsExplorationUrl, + createDataFrameAnalyticsMapUrl, } from './data_frame_analytics_urls_generator'; import { createGenericMlUrl } from './common'; import { createEditCalendarUrl, createEditFilterUrl } from './settings_urls_generator'; @@ -68,6 +69,10 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition model.metadata?.analytics_config?.id === analyticsId + ); + } + + private async getNextLink({ + id, + type, + }: { + id: string; + type: JOB_MAP_NODE_TYPES; + }): Promise { try { if (type === JOB_MAP_NODE_TYPES.INDEX_PATTERN) { // fetch index data @@ -99,19 +130,56 @@ export class AnalyticsManager { } } + private getAnalyticsModelElements( + analyticsId: string + ): { + modelElement?: AnalyticsMapNodeElement; + modelDetails?: any; + edgeElement?: AnalyticsMapEdgeElement; + } { + // Get inference model for analytics job and create model node + const analyticsModel = this.findJobModel(analyticsId); + let modelElement; + let edgeElement; + + if (analyticsModel !== undefined) { + const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.INFERENCE_MODEL}`; + modelElement = { + data: { + id: modelId, + label: analyticsModel.model_id, + type: JOB_MAP_NODE_TYPES.INFERENCE_MODEL, + }, + }; + // Create edge for job and corresponding model + edgeElement = { + data: { + id: `${analyticsId}-${JOB_MAP_NODE_TYPES.ANALYTICS}~${modelId}`, + source: `${analyticsId}-${JOB_MAP_NODE_TYPES.ANALYTICS}`, + target: modelId, + }, + }; + } + + return { modelElement, modelDetails: analyticsModel, edgeElement }; + } + /** * Works backward from jobId to return related jobs from source indices * @param jobId */ - async getAnalyticsMap(analyticsId: string) { + async getAnalyticsMap(analyticsId: string): Promise { const result: any = { elements: [], details: {}, error: null }; + const modelElements: MapElements[] = []; try { + await this.setInferenceModels(); + // Create first node for incoming analyticsId let data = await this.getAnalyticsJobData(analyticsId); let nextLinkId = data?.source?.index[0]; let nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; let complete = false; - let link: any = {}; + let link: NextLinkReturnType; let count = 0; let rootTransform; let rootIndexPattern; @@ -127,10 +195,19 @@ export class AnalyticsManager { }, }); result.details[firstNodeId] = data; + + let { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(analyticsId); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } // Add a safeguard against infinite loops. while (complete === false) { count++; - if (count >= 50) { + if (count >= 100) { break; } @@ -144,7 +221,7 @@ export class AnalyticsManager { break; } // If it's index pattern, check meta data to see what to fetch next - if (link.isIndexPattern === true) { + if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; result.elements.unshift({ data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX_PATTERN }, @@ -167,7 +244,7 @@ export class AnalyticsManager { complete = true; break; } - } else if (link.isJob === true) { + } else if (isJobDataLinkReturnType(link) && link.isJob === true) { data = link.jobData; const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; @@ -182,7 +259,17 @@ export class AnalyticsManager { result.details[nodeId] = data; nextLinkId = data?.source?.index[0]; nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; - } else if (link.isTransform === true) { + + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + } else if (isTransformLinkReturnType(link) && link.isTransform === true) { data = link.transformData; const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; @@ -202,13 +289,15 @@ export class AnalyticsManager { for (let i = 0; i < elemLength; i++) { const currentElem = result.elements[i]; const nextElem = result.elements[i + 1]; - result.elements.push({ - data: { - id: `${currentElem.data.id}~${nextElem.data.id}`, - source: currentElem.data.id, - target: nextElem.data.id, - }, - }); + if (currentElem !== undefined && nextElem !== undefined) { + result.elements.push({ + data: { + id: `${currentElem.data.id}~${nextElem.data.id}`, + source: currentElem.data.id, + target: nextElem.data.id, + }, + }); + } } // fetch all jobs associated with root transform if defined, otherwise check root index @@ -240,9 +329,22 @@ export class AnalyticsManager { target: nodeId, }, }); + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + jobs[i].id + )); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } } } } + // Include model nodes in result elements now that all other nodes have been created + result.elements.push(...modelElements); return result; } catch (error) { @@ -250,7 +352,7 @@ export class AnalyticsManager { return result; } } - + // TODO: add model nodes for extension as well async extendAnalyticsMapForAnalyticsJob(analyticsId: string) { const result: any = { elements: [], details: {}, error: null }; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts new file mode 100644 index 0000000000000..1a1125fe6bc3b --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IndexPatternLinkReturnType { + isIndexPattern: boolean; + indexData: any; + meta: any; +} +export interface JobDataLinkReturnType { + isJob: boolean; + jobData: any; +} +export interface TransformLinkReturnType { + isTransform: boolean; + transformData: any; +} +export type NextLinkReturnType = + | IndexPatternLinkReturnType + | JobDataLinkReturnType + | TransformLinkReturnType + | undefined; +export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement; +export interface AnalyticsMapReturnType { + elements: MapElements[]; + details: object; // transform, job, or index details + error: null | any; +} +export interface AnalyticsMapNodeElement { + data: { + id: string; + label: string; + type: string; + analysisType?: string; + }; +} +export interface AnalyticsMapEdgeElement { + data: { + id: string; + source: string; + target: string; + }; +} +export const isAnalyticsMapNodeElement = (arg: any): arg is AnalyticsMapNodeElement => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('data') && arg.data.label !== undefined; +}; +export const isAnalyticsMapEdgeElement = (arg: any): arg is AnalyticsMapEdgeElement => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('data') && arg.data.target !== undefined; +}; +export const isIndexPatternLinkReturnType = (arg: any): arg is IndexPatternLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isIndexPattern'); +}; + +export const isJobDataLinkReturnType = (arg: any): arg is JobDataLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isJob'); +}; + +export const isTransformLinkReturnType = (arg: any): arg is TransformLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isTransform'); +}; From 3e3099f925aa4ee91e9e38531e7e38b31db6d9ac Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 26 Oct 2020 17:05:22 -0600 Subject: [PATCH 04/14] get models when extending node. deduplicate elements --- .../pages/job_map/job_map.tsx | 4 +++- .../data_frame_analytics/analytics_manager.ts | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 2e17245805766..0734116d1d97a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -9,6 +9,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; import cytoscape from 'cytoscape'; +import { uniqWith, isEqual } from 'lodash'; import { Cytoscape, Controls, JobMapLegend } from './components'; import { ml } from '../../../services/ml_api_service'; @@ -87,7 +88,8 @@ export const JobMap: FC = ({ analyticsId }) => { setElements(nodeElements); setNodeDetails(details); } else { - setElements([...elements, ...nodeElements]); + const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual); + setElements(uniqueElements); setNodeDetails({ ...details, ...nodeDetails }); } } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 2967711d826ce..cc9359b1929f5 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -352,11 +352,13 @@ export class AnalyticsManager { return result; } } - // TODO: add model nodes for extension as well - async extendAnalyticsMapForAnalyticsJob(analyticsId: string) { + + async extendAnalyticsMapForAnalyticsJob(analyticsId: string): Promise { const result: any = { elements: [], details: {}, error: null }; try { + await this.setInferenceModels(); + const jobData = await this.getAnalyticsJobData(analyticsId); const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; const destIndex = Array.isArray(jobData?.dest?.index) @@ -366,6 +368,18 @@ export class AnalyticsManager { const analyticsJobs = await this._client.ml.getDataFrameAnalytics(); const jobs = analyticsJobs?.body?.data_frame_analytics || []; + // Fetch inference model for incoming job id and add node and edge + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + analyticsId + ); + if (isAnalyticsMapNodeElement(modelElement)) { + result.elements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + result.elements.push(edgeElement); + } + // If destIndex node has not been created, create it const destIndexDetails = await this.getIndexData(destIndex); result.elements.push({ From 380dcd7286e6188b9c8fa13bf689ac1c24ef5f04 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 27 Oct 2020 10:53:56 -0600 Subject: [PATCH 05/14] add job type icons. disable map action if job not finished. --- .../components/action_map/use_map_action.tsx | 3 +- .../pages/job_map/components/_legend.scss | 18 ++++++-- .../job_map/components/cytoscape_options.tsx | 42 +++++++++++++++---- .../job_map/components/delete_button.tsx | 2 +- .../icons/ml_classification_job.svg | 7 ++++ .../icons/ml_outlier_detection_job.svg | 7 ++++ .../components/icons/ml_regression_job.svg | 4 ++ .../pages/job_map/components/legend.tsx | 12 ++++++ 8 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx index d4c02c9eb494d..15ef083f5ca50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana'; import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { getViewLinkStatus } from '../action_view/get_view_link_status'; import { mapActionButtonText, MapButton } from './map_button'; @@ -30,7 +31,7 @@ export const useMapAction = () => { () => ({ isPrimary: true, name: (item: DataFrameAnalyticsListRow) => , - enabled: () => true, + enabled: (item: DataFrameAnalyticsListRow) => !getViewLinkStatus(item).disabled, description: mapActionButtonText, icon: 'graphApp', type: 'icon', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss index 4a027f822856c..ed24b6fa8b5d2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss @@ -5,7 +5,8 @@ .mlJobMapLegend__indexPattern { height: $euiSizeM; width: $euiSizeM; - background-color: $euiColorVis2; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis2; transform: rotate(45deg); display: 'inline-block'; } @@ -13,14 +14,25 @@ .mlJobMapLegend__transform { height: $euiSizeM; width: $euiSizeM; - background-color: $euiColorVis1; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis1; display: 'inline-block'; } .mlJobMapLegend__analytics { height: $euiSizeM; width: $euiSizeM; - background-color: $euiColorVis0; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis0; + border-radius: 50%; + display: 'inline-block'; +} + +.mlJobMapLegend__inferenceModel { + height: $euiSizeM; + width: $euiSizeM; + background-color: '#FFFFFF'; + border: 1px solid $euiColorMediumShade; border-radius: 50%; display: 'inline-block'; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index d5a3a3f1689bf..26ec6f769375e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -6,9 +6,15 @@ import cytoscape from 'cytoscape'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../common/constants/data_frame_analytics'; import { JOB_MAP_NODE_TYPES } from '../common'; +import classificationJobIcon from './icons/ml_classification_job.svg'; +import outlierDetectionJobIcon from './icons/ml_outlier_detection_job.svg'; +import regressionJobIcon from './icons/ml_regression_job.svg'; const lineColor = '#C5CCD7'; +// @ts-expect-error `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; enum MAP_SHAPES { ELLIPSE = 'ellipse', @@ -30,17 +36,37 @@ function shapeForNode(el: cytoscape.NodeSingular): MAP_SHAPES { } } -function colorForNode(el: cytoscape.NodeSingular) { +function iconForNode(el: cytoscape.NodeSingular) { + const type = el.data('analysisType'); + + switch (type) { + case ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION: + return outlierDetectionJobIcon; + case ANALYSIS_CONFIG_TYPE.CLASSIFICATION: + return classificationJobIcon; + case ANALYSIS_CONFIG_TYPE.REGRESSION: + return regressionJobIcon; + default: + return undefined; + } +} + +function borderColorForNode(el: cytoscape.NodeSingular) { + if (el.selected()) { + return theme.euiColorPrimary; + } + const type = el.data('type'); + switch (type) { case JOB_MAP_NODE_TYPES.ANALYTICS: - return theme.euiColorVis0; + return theme.euiColorSecondary; case JOB_MAP_NODE_TYPES.TRANSFORM: return theme.euiColorVis1; case JOB_MAP_NODE_TYPES.INDEX_PATTERN: return theme.euiColorVis2; default: - return 'white'; + return theme.euiColorMediumShade; } } @@ -54,12 +80,14 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { { selector: 'node', style: { - 'background-color': (el: cytoscape.NodeSingular) => colorForNode(el), + 'background-color': theme.euiColorGhost, 'background-height': '60%', 'background-width': '60%', - 'border-color': (el: cytoscape.NodeSingular) => - el.selected() ? theme.euiColorPrimary : theme.euiColorMediumShade, - 'border-width': 2, + 'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el), + 'border-style': 'solid', + // @ts-ignore + 'background-image': isIE11 ? undefined : (el: cytoscape.NodeSingular) => iconForNode(el), + 'border-width': (el: cytoscape.NodeSingular) => (el.selected() ? 2 : 1), // @ts-ignore color: theme.textColors.default, 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx index 74ec6067a6349..73bbe518fce07 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx @@ -47,7 +47,7 @@ export const DeleteButton: FC = ({ id, type }) => { return ( - {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton', { + {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteJobButton', { defaultMessage: 'Delete job', })} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg new file mode 100644 index 0000000000000..5659de836b1db --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg @@ -0,0 +1,7 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg new file mode 100644 index 0000000000000..293a0fff7b1ec --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg @@ -0,0 +1,7 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg new file mode 100644 index 0000000000000..9bdc5a79522f7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg @@ -0,0 +1,4 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx index 69ad31b7ee48e..162562bb9b19f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -46,5 +46,17 @@ export const JobMapLegend: FC = () => ( + + + + + + + + {'inference model'} + + + + ); From ded6a59aaf86fc368db55340b69fed1edeff5754 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 28 Oct 2020 10:58:23 -0600 Subject: [PATCH 06/14] move shared const to common dir --- .../ml/common/constants/data_frame_analytics.ts | 7 +++++++ x-pack/plugins/ml/common/util/analytics_utils.ts | 15 ++++++++++++++- .../data_frame_analytics/common/analytics.ts | 14 ++------------ .../data_frame_analytics/pages/job_map/common.ts | 12 ------------ .../pages/job_map/components/controls.tsx | 2 +- .../job_map/components/cytoscape_options.tsx | 6 ++++-- .../pages/job_map/components/delete_button.tsx | 2 +- .../pages/job_map/components/legend.tsx | 2 +- .../data_frame_analytics/analytics_manager.ts | 6 +++--- 9 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 9a7af2496c03f..616eb0aeefd45 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -10,3 +10,10 @@ export const ANALYSIS_CONFIG_TYPE = { CLASSIFICATION: 'classification', } as const; export const DEFAULT_RESULTS_FIELD = 'ml'; + +export enum JOB_MAP_NODE_TYPES { + ANALYTICS = 'analytics', + TRANSFORM = 'transform', + INDEX_PATTERN = 'index-pattern', + INFERENCE_MODEL = 'inferenceModel', +} diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts index 94797efdfcfad..5da9d270a44a7 100644 --- a/x-pack/plugins/ml/common/util/analytics_utils.ts +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -10,7 +10,8 @@ import { OutlierAnalysis, RegressionAnalysis, } from '../types/data_frame_analytics'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../types/data_frame_analytics'; export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { if (typeof arg !== 'object' || arg === null) return false; @@ -80,3 +81,15 @@ export const getPredictedFieldName = ( }`; return predictedField; }; + +export const getAnalysisType = ( + analysis: AnalysisConfig +): DataFrameAnalysisConfigType | 'unknown' => { + const keys = Object.keys(analysis || {}); + + if (keys.length === 1) { + return keys[0] as DataFrameAnalysisConfigType; + } + + return 'unknown'; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 4f13b9a526323..a99270a3e3be6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -27,6 +27,8 @@ import { getPredictedFieldName, } from '../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics'; + +export { getAnalysisType } from '../../../../common/util/analytics_utils'; export type IndexPattern = string; export enum ANALYSIS_ADVANCED_FIELDS { @@ -159,18 +161,6 @@ interface LoadEvaluateResult { error: string | null; } -export const getAnalysisType = ( - analysis: AnalysisConfig -): DataFrameAnalysisConfigType | 'unknown' => { - const keys = Object.keys(analysis || {}); - - if (keys.length === 1) { - return keys[0] as DataFrameAnalysisConfigType; - } - - return 'unknown'; -}; - export const getTrainingPercent = ( analysis: AnalysisConfig ): diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts deleted file mode 100644 index 9e9d8179dcb16..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/common.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum JOB_MAP_NODE_TYPES { - ANALYTICS = 'analytics', - TRANSFORM = 'transform', - INDEX_PATTERN = 'index-pattern', - INFERENCE_MODEL = 'inferenceModel', -} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index 130ba21f18c47..ec3b1ebdec58e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; import { CytoscapeContext } from './cytoscape'; -import { JOB_MAP_NODE_TYPES } from '../common'; +import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; import { DeleteButton } from './delete_button'; interface Props { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index 26ec6f769375e..a6ce5133535de 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -6,8 +6,10 @@ import cytoscape from 'cytoscape'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ANALYSIS_CONFIG_TYPE } from '../../../../../../common/constants/data_frame_analytics'; -import { JOB_MAP_NODE_TYPES } from '../common'; +import { + ANALYSIS_CONFIG_TYPE, + JOB_MAP_NODE_TYPES, +} from '../../../../../../common/constants/data_frame_analytics'; import classificationJobIcon from './icons/ml_classification_job.svg'; import outlierDetectionJobIcon from './icons/ml_outlier_detection_job.svg'; import regressionJobIcon from './icons/ml_regression_job.svg'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx index 73bbe518fce07..80748da84383c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx @@ -9,7 +9,7 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../services/ml_api_service'; import { getToastNotifications } from '../../../../util/dependency_cache'; -import { JOB_MAP_NODE_TYPES } from '../common'; +import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; interface Props { id: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx index 162562bb9b19f..baed088ce4050 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { JOB_MAP_NODE_TYPES } from '../common'; +import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; export const JobMapLegend: FC = () => ( diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index cc9359b1929f5..01fe4d1c6e7a1 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -6,8 +6,8 @@ import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; -import { JOB_MAP_NODE_TYPES } from '../../../public/application/data_frame_analytics/pages/job_map/common'; // eslint-disable-line -import { getAnalysisType } from '../../../public/application/data_frame_analytics/common/analytics'; // eslint-disable-line +import { JOB_MAP_NODE_TYPES } from '../../../common/constants/data_frame_analytics'; +import { getAnalysisType } from '../../../common/util/analytics_utils'; import { AnalyticsMapEdgeElement, AnalyticsMapReturnType, @@ -58,7 +58,7 @@ export class AnalyticsManager { }); return isDuplicate; } - // @ts-ignore + // @ts-ignore // TODO: is this needed? private async getAnalyticsModelData(modelId: string) { const resp = await this._client.ml.getTrainedModels({ model_id: modelId, From 1d7341fa3ffec5c271b888097451e6b170b40aa4 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 29 Oct 2020 10:23:09 -0600 Subject: [PATCH 07/14] persist map tab. handle indexPattern from visualizer --- .../analytics_navigation_bar.tsx | 2 +- .../pages/analytics_management/page.tsx | 26 +++++++++++++++---- .../data_frame_analytics/analytics_map.tsx | 20 ++------------ .../data_frame_analytics/analytics_manager.ts | 3 ++- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index 03c39caf69a8c..eaeae6cc64520 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -48,7 +48,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }); } return navTabs; - }, []); + }, [jobId !== undefined]); const onTabClick = useCallback(async (tab: Tab) => { await navigateToPath(tab.path, true); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 964ca7d631273..f31fa8012865a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -8,6 +8,8 @@ import React, { FC, Fragment, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; +import { decode } from 'rison-node'; import { EuiBetaBadge, @@ -33,15 +35,29 @@ import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; import { JobMap } from '../job_map'; -export const Page: FC<{ - jobId?: string; -}> = ({ jobId }) => { +export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); useRefreshInterval(setBlockRefresh); const location = useLocation(); const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]); + const mapJobId = useMemo(() => { + const { _g }: Record = parse(location.search, { sort: false }); + let jobId: string | undefined; + + if (_g !== undefined) { + let globalState: any = null; + try { + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global state'); + } + jobId = globalState.ml?.jobId; + } + return jobId; + }, [location]); return ( @@ -90,8 +106,8 @@ export const Page: FC<{ - - {selectedTabId === 'map' && jobId && } + + {selectedTabId === 'map' && mapJobId && } {selectedTabId === 'data_frame_analytics' && ( )} diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx index 0788de4de2913..18002648cfaa6 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx @@ -6,8 +6,6 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { parse } from 'query-string'; -import { decode } from 'rison-node'; import { NavigateToPath } from '../../../contexts/kibana'; @@ -35,26 +33,12 @@ export const analyticsMapRouteFactory = ( ], }); -const PageWrapper: FC = ({ location, deps }) => { +const PageWrapper: FC = ({ deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); - const { _g }: Record = parse(location.search, { sort: false }); - let jobId; - - if (_g !== undefined) { - let globalState: any = null; - try { - globalState = decode(_g); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Could not parse global state'); - window.location.href = '#data_frame_analytics'; - } - jobId = globalState.ml?.jobId; - } return ( - + ); }; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 01fe4d1c6e7a1..92c5d315cb5a3 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -7,6 +7,7 @@ import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; import { JOB_MAP_NODE_TYPES } from '../../../common/constants/data_frame_analytics'; +import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { AnalyticsMapEdgeElement, @@ -239,7 +240,7 @@ export class AnalyticsManager { } // Check meta data - if (link.meta === undefined) { + if (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) { rootIndexPattern = nextLinkId; complete = true; break; From 05b82ecf15234c9737f0c48c9b757b2e0eea03de Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 29 Oct 2020 11:34:37 -0600 Subject: [PATCH 08/14] use url generator in models list --- .../ml/common/types/ml_url_generator.ts | 2 +- .../components/action_map/use_map_action.tsx | 1 - .../components/analytics_list/common.ts | 4 ---- .../models_management/models_list.tsx | 20 +++++++++++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index aa38fb2ec6fbb..b188ac0a87571 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -159,7 +159,7 @@ export interface DataFrameAnalyticsQueryState { } export type DataFrameAnalyticsUrlState = MLPageState< - typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE | typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP, DataFrameAnalyticsQueryState | undefined >; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx index 15ef083f5ca50..f77f71dcee4e7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -19,7 +19,6 @@ export const useMapAction = () => { const clickHandler = useCallback(async (item: DataFrameAnalyticsListRow) => { const path = await mlUrlGenerator.createUrl({ - // @ts-ignore page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, pageState: { jobId: item.id }, }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index d5b75c4c7659b..8c7c8b9db8b64 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -132,10 +132,6 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) { return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100; } -export function getJobMapUrl(jobId: string) { - return `#/data_frame_analytics/map?_g=(ml:(jobId:${jobId}))`; -} - // The single Action type is not exported as is // from EUI so we use that code to get the single // Action type from the array of actions. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 86691d45ef44f..a87f11df937d3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -28,9 +28,14 @@ import { StatsBar, ModelsBarStats } from '../../../../../components/stats_bar'; import { useTrainedModelsApiService } from '../../../../../services/ml_api_service/trained_models'; import { ModelsTableToConfigMapping } from './index'; import { DeleteModelsModal } from './delete_models_modal'; -import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana'; +import { + useMlKibana, + useMlUrlGenerator, + useNavigateToPath, + useNotifications, +} from '../../../../../contexts/kibana'; import { ExpandedRow } from './expanded_row'; -import { getJobMapUrl } from '../analytics_list/common'; + import { TrainedModelConfigResponse, ModelPipelines, @@ -81,6 +86,9 @@ export const ModelsList: FC = () => { {} ); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const updateFilteredItems = (queryClauses: any) => { if (queryClauses.length) { const filtered = filterAnalyticsModels(items, queryClauses); @@ -311,8 +319,12 @@ export const ModelsList: FC = () => { isPrimary: true, available: (item) => item.metadata?.analytics_config?.id, onClick: async (item) => { - // TODO: update to use new navigation? - await navigateToUrl(getJobMapUrl(item.metadata?.analytics_config.id)); + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, + pageState: { jobId: item.metadata?.analytics_config.id }, + }); + + await navigateToPath(path, false); }, }, { From 701886a8cf23ab785b04ac713baeabdf5c881e2b Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 29 Oct 2020 11:52:35 -0600 Subject: [PATCH 09/14] temporarily disable delete action in flyout --- .../pages/job_map/components/controls.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index ec3b1ebdec58e..95c2837dd4546 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { CytoscapeContext } from './cytoscape'; import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; -import { DeleteButton } from './delete_button'; +// import { DeleteButton } from './delete_button'; interface Props { analyticsId: string; @@ -140,9 +140,9 @@ export const Controls: FC = ({ analyticsId, details, getNodeData }) => { {nodeDataButton} - + {/* - + */} From 3dbf6a486cb1d0a1fa8652a37879656a57c1d4a4 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 2 Nov 2020 19:49:32 -0700 Subject: [PATCH 10/14] update legend style. make map horizontal --- .../common/constants/data_frame_analytics.ts | 14 +++++++------ .../pages/job_map/components/_legend.scss | 2 +- .../pages/job_map/components/cytoscape.tsx | 20 +++++-------------- .../job_map/components/cytoscape_options.tsx | 19 +++++++++--------- .../job_map/components/delete_button.tsx | 7 +++++-- .../job_map/components/use_ref_dimensions.ts | 2 +- .../data_frame_analytics/analytics_manager.ts | 11 ++++++---- 7 files changed, 36 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 616eb0aeefd45..7d8533482c8ab 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -11,9 +11,11 @@ export const ANALYSIS_CONFIG_TYPE = { } as const; export const DEFAULT_RESULTS_FIELD = 'ml'; -export enum JOB_MAP_NODE_TYPES { - ANALYTICS = 'analytics', - TRANSFORM = 'transform', - INDEX_PATTERN = 'index-pattern', - INFERENCE_MODEL = 'inferenceModel', -} +export const JOB_MAP_NODE_TYPES = { + ANALYTICS: 'analytics', + TRANSFORM: 'transform', + INDEX_PATTERN: 'index-pattern', + INFERENCE_MODEL: 'inferenceModel', +} as const; + +export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss index ed24b6fa8b5d2..d54b5214f7448 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss @@ -1,5 +1,5 @@ .mlJobMapLegend__container { - background-color: $euiColorLightestShade; + background-color: '#FFFFFF'; } .mlJobMapLegend__indexPattern { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index 45e62c868547a..8409b30598964 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -14,8 +14,11 @@ import React, { useCallback, } from 'react'; import cytoscape from 'cytoscape'; +import dagre from 'cytoscape-dagre'; import { cytoscapeOptions } from './cytoscape_options'; +cytoscape.use(dagre); + export const CytoscapeContext = createContext(undefined); interface CytoscapeProps { @@ -50,8 +53,8 @@ function useCytoscape(options: cytoscape.CytoscapeOptions) { function getLayoutOptions(width: number, height: number) { return { - name: 'breadthfirst', - directed: true, + name: 'dagre', + rankDir: 'LR', fit: true, spacingFactor: 0.85, boundingBox: { x1: 0, y1: 0, w: width, h: height }, @@ -80,26 +83,13 @@ export function Cytoscape({ children, elements, height, style, width }: Cytoscap // Set up cytoscape event handlers useEffect(() => { - const mouseoverHandler: cytoscape.EventHandler = (event) => { - event.target.addClass('hover'); - event.target.connectedEdges().addClass('nodeHover'); - }; - const mouseoutHandler: cytoscape.EventHandler = (event) => { - event.target.removeClass('hover'); - event.target.connectedEdges().removeClass('nodeHover'); - }; - if (cy) { cy.on('data', dataHandler); - cy.on('mouseover', 'edge, node', mouseoverHandler); - cy.on('mouseout', 'edge, node', mouseoutHandler); } return () => { if (cy) { cy.removeListener('data', undefined, dataHandler as cytoscape.EventHandler); - cy.removeListener('mouseover', 'edge, node', mouseoverHandler); - cy.removeListener('mouseout', 'edge, node', mouseoutHandler); } }; }, [cy, elements, height, width]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index a6ce5133535de..6467c9f7952a4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -15,16 +15,15 @@ import outlierDetectionJobIcon from './icons/ml_outlier_detection_job.svg'; import regressionJobIcon from './icons/ml_regression_job.svg'; const lineColor = '#C5CCD7'; -// @ts-expect-error `documentMode` is not recognized as a valid property of `document`. -const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; -enum MAP_SHAPES { - ELLIPSE = 'ellipse', - RECTANGLE = 'rectangle', - DIAMOND = 'diamond', -} +const MAP_SHAPES = { + ELLIPSE: 'ellipse', + RECTANGLE: 'rectangle', + DIAMOND: 'diamond', +} as const; +type MapShapes = typeof MAP_SHAPES[keyof typeof MAP_SHAPES]; -function shapeForNode(el: cytoscape.NodeSingular): MAP_SHAPES { +function shapeForNode(el: cytoscape.NodeSingular): MapShapes { const type = el.data('type'); switch (type) { case JOB_MAP_NODE_TYPES.ANALYTICS: @@ -88,7 +87,7 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { 'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el), 'border-style': 'solid', // @ts-ignore - 'background-image': isIE11 ? undefined : (el: cytoscape.NodeSingular) => iconForNode(el), + 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el), 'border-width': (el: cytoscape.NodeSingular) => (el.selected() ? 2 : 1), // @ts-ignore color: theme.textColors.default, @@ -107,7 +106,7 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { 'text-margin-y': theme.paddingSizes.s, 'text-max-width': '200px', 'text-valign': 'bottom', - 'text-wrap': 'ellipsis', + 'text-wrap': 'wrap', }, }, { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx index 80748da84383c..523d24a3a3981 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx @@ -9,11 +9,14 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../services/ml_api_service'; import { getToastNotifications } from '../../../../util/dependency_cache'; -import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; +import { + JOB_MAP_NODE_TYPES, + JobMapNodeTypes, +} from '../../../../../../common/constants/data_frame_analytics'; interface Props { id: string; - type: JOB_MAP_NODE_TYPES; + type: JobMapNodeTypes; } export const DeleteButton: FC = ({ id, type }) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts index c8639b334f66a..fc478e27ccac3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { useRef } from 'react'; -import { useWindowSize } from 'react-use'; +import useWindowSize from 'react-use/lib/useWindowSize'; export function useRefDimensions() { const ref = useRef(null); diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 92c5d315cb5a3..04e0fadc0e333 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; +import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; -import { JOB_MAP_NODE_TYPES } from '../../../common/constants/data_frame_analytics'; +import { + JOB_MAP_NODE_TYPES, + JobMapNodeTypes, +} from '../../../common/constants/data_frame_analytics'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { @@ -109,7 +112,7 @@ export class AnalyticsManager { type, }: { id: string; - type: JOB_MAP_NODE_TYPES; + type: JobMapNodeTypes; }): Promise { try { if (type === JOB_MAP_NODE_TYPES.INDEX_PATTERN) { @@ -178,7 +181,7 @@ export class AnalyticsManager { // Create first node for incoming analyticsId let data = await this.getAnalyticsJobData(analyticsId); let nextLinkId = data?.source?.index[0]; - let nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX_PATTERN; let complete = false; let link: NextLinkReturnType; let count = 0; From 747543428bb6f5f62d455dfb2a3715e1449dc493 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 4 Nov 2020 16:44:47 -0700 Subject: [PATCH 11/14] update dfa model to use spaces changes --- .../components/action_map/map_button.tsx | 13 ++++---- .../pages/analytics_management/page.tsx | 21 ++----------- .../pages/job_map/components/controls.tsx | 31 ++++++++----------- .../pages/job_map/components/cytoscape.tsx | 2 +- .../pages/job_map/job_map.tsx | 12 ++++--- .../data_frame_analytics/analytics_manager.ts | 15 +++++---- .../ml/server/routes/data_frame_analytics.ts | 11 ++++--- 7 files changed, 46 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx index 4015d03cc6dcc..28016b421aff3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx @@ -27,18 +27,19 @@ interface MapButtonProps { } export const MapButton: FC = ({ item }) => { - const toolTipContent = i18n.translate( - 'xpack.ml.dataframe.analyticsList.mapActionDisabledTooltipContent', - { - defaultMessage: 'Unknown analysis type.', - } - ); const disabled = !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis) && !isClassificationAnalysis(item.config.analysis); if (disabled) { + const toolTipContent = i18n.translate( + 'xpack.ml.dataframe.analyticsList.mapActionDisabledTooltipContent', + { + defaultMessage: 'Unknown analysis type.', + } + ); + return ( <>{mapActionButtonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index f31fa8012865a..2cc0dee0354b2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -8,8 +8,6 @@ import React, { FC, Fragment, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { parse } from 'query-string'; -import { decode } from 'rison-node'; import { EuiBetaBadge, @@ -24,6 +22,7 @@ import { } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; +import { useUrlState } from '../../../util/url_state'; import { NavigationMenu } from '../../../components/navigation_menu'; import { DatePickerWrapper } from '../../../components/navigation_menu/date_picker_wrapper'; import { DataFrameAnalyticsList } from './components/analytics_list'; @@ -37,27 +36,13 @@ import { JobMap } from '../job_map'; export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); + const [globalState] = useUrlState('_g'); useRefreshInterval(setBlockRefresh); const location = useLocation(); const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]); - const mapJobId = useMemo(() => { - const { _g }: Record = parse(location.search, { sort: false }); - let jobId: string | undefined; - - if (_g !== undefined) { - let globalState: any = null; - try { - globalState = decode(_g); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Could not parse global state'); - } - jobId = globalState.ml?.jobId; - } - return jobId; - }, [location]); + const mapJobId = globalState?.ml?.jobId; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index 95c2837dd4546..2e1df8055cdd8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -6,7 +6,7 @@ import React, { FC, useEffect, useState, useContext, useCallback } from 'react'; import cytoscape from 'cytoscape'; -import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiDescriptionList, @@ -19,6 +19,7 @@ import { EuiPortal, EuiTitle, } from '@elastic/eui'; +import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { CytoscapeContext } from './cytoscape'; import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; // import { DeleteButton } from './delete_button'; @@ -29,13 +30,7 @@ interface Props { getNodeData: any; } -// TODO move list items to shared place as we use them in the details bit of the wizard -export interface ListItems { - title: string; - description: string | JSX.Element; -} - -function getListItems(details: object): ListItems[] { +function getListItems(details: object): EuiDescriptionListProps['listItems'] { return Object.entries(details).map(([key, value]) => ({ title: key, description: typeof value === 'object' ? JSON.stringify(value, null, 2) : value, @@ -44,7 +39,7 @@ function getListItems(details: object): ListItems[] { export const Controls: FC = ({ analyticsId, details, getNodeData }) => { const [showFlyout, setShowFlyout] = useState(false); - const [selectedNode, setSelectedNode] = useState(undefined); + const [selectedNode, setSelectedNode] = useState(); const cy = useContext(CytoscapeContext); const deselect = useCallback(() => { @@ -69,14 +64,12 @@ export const Controls: FC = ({ analyticsId, details, getNodeData }) => { if (cy) { cy.on('select', 'node', selectHandler); cy.on('unselect', 'node', deselect); - // cy.on('data viewport', deselect); } return () => { if (cy) { cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', deselect); - // cy.removeListener('data viewport', undefined, deselect); } }; }, [cy, deselect]); @@ -94,9 +87,10 @@ export const Controls: FC = ({ analyticsId, details, getNodeData }) => { }} iconType="branch" > - {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton', { - defaultMessage: 'Fetch related nodes', - })} + ) : null; @@ -113,10 +107,11 @@ export const Controls: FC = ({ analyticsId, details, getNodeData }) => {

- {i18n.translate('xpack.ml.dataframe.analyticsMap.flyoutHeaderTitle', { - defaultMessage: 'Details for {type} {id}', - values: { id: nodeLabel, type: nodeType }, - })} +

diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index 8409b30598964..e21c0874bca64 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -30,7 +30,7 @@ interface CytoscapeProps { } function useCytoscape(options: cytoscape.CytoscapeOptions) { - const [cy, setCy] = useState(undefined); + const [cy, setCy] = useState(); const ref = useRef(null); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 0734116d1d97a..53d47937409d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -13,8 +13,7 @@ import { uniqWith, isEqual } from 'lodash'; import { Cytoscape, Controls, JobMapLegend } from './components'; import { ml } from '../../../services/ml_api_service'; -// TODO: don't use dep cache - switch to hook -import { getToastNotifications } from '../../../util/dependency_cache'; +import { useMlKibana } from '../../../contexts/kibana'; import { useRefDimensions } from './components/use_ref_dimensions'; const cytoscapeDivStyle = { @@ -53,11 +52,14 @@ interface Props { } export const JobMap: FC = ({ analyticsId }) => { - const toastNotifications = getToastNotifications(); const [elements, setElements] = useState([]); const [nodeDetails, setNodeDetails] = useState({}); const [error, setError] = useState(undefined); + const { + services: { notifications }, + } = useMlKibana(); + const getData = async (id?: string) => { const treatAsRoot = id !== undefined; const idToUse = treatAsRoot ? id : analyticsId; @@ -75,7 +77,7 @@ export const JobMap: FC = ({ analyticsId }) => { } if (nodeElements && nodeElements.length === 0) { - toastNotifications.add( + notifications.toasts.add( i18n.translate('xpack.ml.dataframe.analyticsMap.emptyResponseMessage', { defaultMessage: 'No related analytics jobs found for {id}.', values: { id: idToUse }, @@ -100,7 +102,7 @@ export const JobMap: FC = ({ analyticsId }) => { }, [analyticsId]); if (error !== undefined) { - toastNotifications.addDanger( + notifications.toasts.addDanger( i18n.translate('xpack.ml.dataframe.analyticsMap.fetchDataErrorMessage', { defaultMessage: 'Unable to fetch some data. An error occurred: {error}', values: { error: JSON.stringify(error) }, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 04e0fadc0e333..9026d47bc81f0 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -24,13 +24,16 @@ import { MapElements, NextLinkReturnType, } from './types'; +import type { MlClient } from '../../lib/ml_client'; export class AnalyticsManager { private _client: IScopedClusterClient['asInternalUser']; + private _mlClient: MlClient; public _inferenceModels: any; // TODO: update types - constructor(client: IScopedClusterClient['asInternalUser']) { + constructor(mlClient: MlClient, client: IScopedClusterClient['asInternalUser']) { this._client = client; + this._mlClient = mlClient; this._inferenceModels = []; } @@ -64,7 +67,7 @@ export class AnalyticsManager { } // @ts-ignore // TODO: is this needed? private async getAnalyticsModelData(modelId: string) { - const resp = await this._client.ml.getTrainedModels({ + const resp = await this._mlClient.getTrainedModels({ model_id: modelId, }); const modelData = resp?.body?.trained_model_configs[0]; @@ -72,13 +75,13 @@ export class AnalyticsManager { } private async getAnalyticsModels() { - const resp = await this._client.ml.getTrainedModels(); + const resp = await this._mlClient.getTrainedModels(); const models = resp?.body?.trained_model_configs; return models; } private async getAnalyticsJobData(analyticsId: string) { - const resp = await this._client.ml.getDataFrameAnalytics({ + const resp = await this._mlClient.getDataFrameAnalytics({ id: analyticsId, }); const jobData = resp?.body?.data_frame_analytics[0]; @@ -306,7 +309,7 @@ export class AnalyticsManager { // fetch all jobs associated with root transform if defined, otherwise check root index if (rootTransform !== undefined || rootIndexPattern !== undefined) { - const analyticsJobs = await this._client.ml.getDataFrameAnalytics(); + const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); const jobs = analyticsJobs?.body?.data_frame_analytics || []; const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; @@ -369,7 +372,7 @@ export class AnalyticsManager { ? jobData?.dest?.index[0] : jobData?.dest?.index; const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; - const analyticsJobs = await this._client.ml.getDataFrameAnalytics(); + const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); const jobs = analyticsJobs?.body?.data_frame_analytics || []; // Fetch inference model for incoming job id and add node and edge diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 232844babf406..8e00ae7068403 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -24,6 +24,7 @@ import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manag import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; import { getAuthorizationHeader } from '../lib/request_authorization'; import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; +import type { MlClient } from '../lib/ml_client'; function getIndexPatternId(context: RequestHandlerContext, patternName: string) { const iph = new IndexPatternHandler(context.core.savedObjects.client); @@ -35,13 +36,13 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern return iph.deleteIndexPatternById(indexPatternId); } -function getAnalyticsMap(client: IScopedClusterClient, analyticsId: string) { - const analytics = new AnalyticsManager(client.asInternalUser); +function getAnalyticsMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(mlClient, client.asInternalUser); return analytics.getAnalyticsMap(analyticsId); } -function getExtendedMap(client: IScopedClusterClient, analyticsId: string) { - const analytics = new AnalyticsManager(client.asInternalUser); +function getExtendedMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(mlClient, client.asInternalUser); return analytics.extendAnalyticsMapForAnalyticsJob(analyticsId); } @@ -634,6 +635,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout const treatAsRoot = request.query?.treatAsRoot; const caller = treatAsRoot === 'true' || treatAsRoot === true ? getExtendedMap : getAnalyticsMap; + const results = await caller(mlClient, client, analyticsId); return response.ok({ @@ -645,4 +647,3 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout }) ); } -} From fd17d547a85bf8f3a81aee5808653ffd76705bb0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 6 Nov 2020 11:33:21 -0700 Subject: [PATCH 12/14] format creation time --- .../pages/job_map/components/controls.tsx | 27 ++++++++++++++++--- .../pages/job_map/components/cytoscape.tsx | 8 +++--- .../job_map/components/cytoscape_options.tsx | 8 ++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index 2e1df8055cdd8..ed25ea6cbf02c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -7,8 +7,10 @@ import React, { FC, useEffect, useState, useContext, useCallback } from 'react'; import cytoscape from 'cytoscape'; import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment-timezone'; import { EuiButtonEmpty, + EuiCodeBlock, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, @@ -21,6 +23,7 @@ import { } from '@elastic/eui'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; 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'; @@ -31,10 +34,26 @@ interface Props { } function getListItems(details: object): EuiDescriptionListProps['listItems'] { - return Object.entries(details).map(([key, value]) => ({ - title: key, - description: typeof value === 'object' ? JSON.stringify(value, null, 2) : value, - })); + return Object.entries(details).map(([key, value]) => { + let description; + if (key === 'create_time') { + description = formatHumanReadableDateTimeSeconds(moment(value).unix() * 1000); + } else { + description = + typeof value === 'object' ? ( + + {JSON.stringify(value, null, 2)} + + ) : ( + value + ); + } + + return { + title: key, + description, + }; + }); } export const Controls: FC = ({ analyticsId, details, getNodeData }) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index e21c0874bca64..598aaeb9715c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -56,7 +56,8 @@ function getLayoutOptions(width: number, height: number) { name: 'dagre', rankDir: 'LR', fit: true, - spacingFactor: 0.85, + padding: 30, + spacingFactor: 0.65, boundingBox: { x1: 0, y1: 0, w: width, h: height }, }; } @@ -73,9 +74,8 @@ export function Cytoscape({ children, elements, height, style, width }: Cytoscap const dataHandler = useCallback( (event) => { - if (cy) { - const layout = cy.layout(getLayoutOptions(width, height)); - layout.run(); + if (cy && height > 0) { + cy.layout(getLayoutOptions(width, height)).run(); } }, [cy, height, width] diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index 6467c9f7952a4..8eb7b36127806 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -76,7 +76,6 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { boxSelectionEnabled: false, maxZoom: 3, minZoom: 0.2, - // @ts-ignore style: [ { selector: 'node', @@ -93,17 +92,14 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { color: theme.textColors.default, 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', 'font-size': theme.euiFontSizeXS, - // @ts-ignore - 'min-zoomed-font-size': theme.euiSizeL, + 'min-zoomed-font-size': parseInt(theme.euiSizeL, 10), label: 'data(label)', shape: (el: cytoscape.NodeSingular) => shapeForNode(el), 'text-background-color': theme.euiColorLightestShade, 'text-background-opacity': 0, - // @ts-ignore 'text-background-padding': theme.paddingSizes.xs, 'text-background-shape': 'roundrectangle', - // @ts-ignore - 'text-margin-y': theme.paddingSizes.s, + 'text-margin-y': parseInt(theme.paddingSizes.s, 10), 'text-max-width': '200px', 'text-valign': 'bottom', 'text-wrap': 'wrap', From d763ec1e66e664812b42a05be0c68bce74b8ad24 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 10 Nov 2020 10:19:37 -0700 Subject: [PATCH 13/14] update from indexPattern to index.remove refresh button --- .../common/constants/data_frame_analytics.ts | 2 +- .../pages/analytics_management/page.tsx | 8 +++++--- .../pages/job_map/components/cytoscape.tsx | 1 + .../job_map/components/cytoscape_options.tsx | 4 ++-- .../pages/job_map/components/legend.tsx | 2 +- .../data_frame_analytics/analytics_manager.ts | 18 +++++++++--------- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 7d8533482c8ab..5c8000566bb38 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -14,7 +14,7 @@ export const DEFAULT_RESULTS_FIELD = 'ml'; export const JOB_MAP_NODE_TYPES = { ANALYTICS: 'analytics', TRANSFORM: 'transform', - INDEX_PATTERN: 'index-pattern', + INDEX: 'index', INFERENCE_MODEL: 'inferenceModel', } as const; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 2cc0dee0354b2..44085384f7536 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -77,9 +77,11 @@ export const Page: FC = () => { - - - + {selectedTabId !== 'map' && ( + + + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index 598aaeb9715c2..b68b4220670f1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -14,6 +14,7 @@ import React, { useCallback, } from 'react'; import cytoscape from 'cytoscape'; +// @ts-ignore no declaration file import dagre from 'cytoscape-dagre'; import { cytoscapeOptions } from './cytoscape_options'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index 8eb7b36127806..85d10aa897415 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -30,7 +30,7 @@ function shapeForNode(el: cytoscape.NodeSingular): MapShapes { return MAP_SHAPES.ELLIPSE; case JOB_MAP_NODE_TYPES.TRANSFORM: return MAP_SHAPES.RECTANGLE; - case JOB_MAP_NODE_TYPES.INDEX_PATTERN: + case JOB_MAP_NODE_TYPES.INDEX: return MAP_SHAPES.DIAMOND; default: return MAP_SHAPES.ELLIPSE; @@ -64,7 +64,7 @@ function borderColorForNode(el: cytoscape.NodeSingular) { return theme.euiColorSecondary; case JOB_MAP_NODE_TYPES.TRANSFORM: return theme.euiColorVis1; - case JOB_MAP_NODE_TYPES.INDEX_PATTERN: + case JOB_MAP_NODE_TYPES.INDEX: return theme.euiColorVis2; default: return theme.euiColorMediumShade; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx index baed088ce4050..c29b6aca804d7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -17,7 +17,7 @@ export const JobMapLegend: FC = () => ( - {JOB_MAP_NODE_TYPES.INDEX_PATTERN} + {JOB_MAP_NODE_TYPES.INDEX} diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 9026d47bc81f0..d894a2ffb2d71 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -118,7 +118,7 @@ export class AnalyticsManager { type: JobMapNodeTypes; }): Promise { try { - if (type === JOB_MAP_NODE_TYPES.INDEX_PATTERN) { + if (type === JOB_MAP_NODE_TYPES.INDEX) { // fetch index data const indexData = await this.getIndexData(id); const meta = indexData[id].mappings._meta; @@ -184,7 +184,7 @@ export class AnalyticsManager { // Create first node for incoming analyticsId let data = await this.getAnalyticsJobData(analyticsId); let nextLinkId = data?.source?.index[0]; - let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; let complete = false; let link: NextLinkReturnType; let count = 0; @@ -229,9 +229,9 @@ export class AnalyticsManager { } // If it's index pattern, check meta data to see what to fetch next if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { - const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; result.elements.unshift({ - data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX_PATTERN }, + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, }); result.details[nodeId] = link.indexData; @@ -265,7 +265,7 @@ export class AnalyticsManager { }); result.details[nodeId] = data; nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + nextType = JOB_MAP_NODE_TYPES.INDEX; // Get inference model for analytics job and create model node ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); @@ -287,7 +287,7 @@ export class AnalyticsManager { }); result.details[nodeId] = data; nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX_PATTERN; + nextType = JOB_MAP_NODE_TYPES.INDEX; } } // end while @@ -328,7 +328,7 @@ export class AnalyticsManager { }, }); result.details[nodeId] = jobs[i]; - const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; result.elements.push({ data: { id: `${source}~${nodeId}`, @@ -371,7 +371,7 @@ export class AnalyticsManager { const destIndex = Array.isArray(jobData?.dest?.index) ? jobData?.dest?.index[0] : jobData?.dest?.index; - const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX_PATTERN}`; + const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); const jobs = analyticsJobs?.body?.data_frame_analytics || []; @@ -393,7 +393,7 @@ export class AnalyticsManager { data: { id: destIndexNodeId, label: destIndex, - type: JOB_MAP_NODE_TYPES.INDEX_PATTERN, + type: JOB_MAP_NODE_TYPES.INDEX, }, }); result.details[destIndexNodeId] = destIndexDetails; From 39728e2d0ca7e47821b00b561d99bac9b502254f Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 10 Nov 2020 17:32:47 -0700 Subject: [PATCH 14/14] handle index patterns with wildcard --- .../pages/job_map/components/cytoscape.tsx | 2 +- .../data_frame_analytics/analytics_manager.ts | 92 +++++++++++++++---- .../models/data_frame_analytics/types.ts | 1 + 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index b68b4220670f1..a901e2be06dc0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -58,7 +58,7 @@ function getLayoutOptions(width: number, height: number) { rankDir: 'LR', fit: true, padding: 30, - spacingFactor: 0.65, + spacingFactor: 0.85, boundingBox: { x1: 0, y1: 0, w: width, h: height }, }; } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index d894a2ffb2d71..f1f0b352ca920 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -121,8 +121,13 @@ export class AnalyticsManager { if (type === JOB_MAP_NODE_TYPES.INDEX) { // fetch index data const indexData = await this.getIndexData(id); - const meta = indexData[id].mappings._meta; - return { isIndexPattern: true, indexData, meta }; + let isWildcardIndexPattern = false; + + if (id.includes('*')) { + isWildcardIndexPattern = true; + } + const meta = indexData[id]?.mappings?._meta; + return { isWildcardIndexPattern, isIndexPattern: true, indexData, meta }; } else if (type.includes(JOB_MAP_NODE_TYPES.ANALYTICS)) { // fetch job associated with this index const jobData = await this.getAnalyticsJobData(id); @@ -171,6 +176,30 @@ export class AnalyticsManager { return { modelElement, modelDetails: analyticsModel, edgeElement }; } + private getIndexPatternElements(indexData: Record, previousNodeId: string) { + const result: any = { elements: [], details: {} }; + + Object.keys(indexData).forEach((indexId) => { + // Create index node + const nodeId = `${indexId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.push({ + data: { id: nodeId, label: indexId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = indexData[indexId]; + + // create edge node + result.elements.push({ + data: { + id: `${previousNodeId}~${nodeId}`, + source: nodeId, + target: previousNodeId, + }, + }); + }); + + return result; + } + /** * Works backward from jobId to return related jobs from source indices * @param jobId @@ -178,6 +207,7 @@ export class AnalyticsManager { async getAnalyticsMap(analyticsId: string): Promise { const result: any = { elements: [], details: {}, error: null }; const modelElements: MapElements[] = []; + const indexPatternElements: MapElements[] = []; try { await this.setInferenceModels(); @@ -191,17 +221,17 @@ export class AnalyticsManager { let rootTransform; let rootIndexPattern; - const firstNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + let previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; result.elements.push({ data: { - id: firstNodeId, + id: previousNodeId, label: data.id, type: JOB_MAP_NODE_TYPES.ANALYTICS, analysisType: getAnalysisType(data?.analysis), }, }); - result.details[firstNodeId] = data; + result.details[previousNodeId] = data; let { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(analyticsId); if (isAnalyticsMapNodeElement(modelElement)) { @@ -229,11 +259,33 @@ export class AnalyticsManager { } // If it's index pattern, check meta data to see what to fetch next if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { - const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; - result.elements.unshift({ - data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, - }); - result.details[nodeId] = link.indexData; + if (link.isWildcardIndexPattern === true) { + // Create index nodes for each of the indices included in the index pattern then break + const { details, elements } = this.getIndexPatternElements( + link.indexData, + previousNodeId + ); + + indexPatternElements.push(...elements); + result.details = { ...result.details, ...details }; + complete = true; + } else { + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.unshift({ + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = link.indexData; + } + + // Check meta data + if ( + link.isWildcardIndexPattern === false && + (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) + ) { + rootIndexPattern = nextLinkId; + complete = true; + break; + } if (link.meta?.created_by === 'data-frame-analytics') { nextLinkId = link.meta.analytics; @@ -244,16 +296,10 @@ export class AnalyticsManager { nextLinkId = link.meta._transform?.transform; nextType = JOB_MAP_NODE_TYPES.TRANSFORM; } - - // Check meta data - if (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) { - rootIndexPattern = nextLinkId; - complete = true; - break; - } } else if (isJobDataLinkReturnType(link) && link.isJob === true) { data = link.jobData; const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + previousNodeId = nodeId; result.elements.unshift({ data: { @@ -280,6 +326,7 @@ export class AnalyticsManager { data = link.transformData; const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; + previousNodeId = nodeId; rootTransform = data.dest.index; result.elements.unshift({ @@ -296,7 +343,12 @@ export class AnalyticsManager { for (let i = 0; i < elemLength; i++) { const currentElem = result.elements[i]; const nextElem = result.elements[i + 1]; - if (currentElem !== undefined && nextElem !== undefined) { + if ( + currentElem !== undefined && + nextElem !== undefined && + currentElem?.data?.id.includes('*') === false && + nextElem?.data?.id.includes('*') === false + ) { result.elements.push({ data: { id: `${currentElem.data.id}~${nextElem.data.id}`, @@ -350,8 +402,8 @@ export class AnalyticsManager { } } } - // Include model nodes in result elements now that all other nodes have been created - result.elements.push(...modelElements); + // Include model and index pattern nodes in result elements now that all other nodes have been created + result.elements.push(...modelElements, ...indexPatternElements); return result; } catch (error) { diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts index 1a1125fe6bc3b..5d6cec8cdfa61 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts @@ -5,6 +5,7 @@ */ export interface IndexPatternLinkReturnType { + isWildcardIndexPattern: boolean; isIndexPattern: boolean; indexData: any; meta: any;