From 38c3593d0b7876dc653f46a4cd695d5efb00de7a Mon Sep 17 00:00:00 2001 From: Dmitrii Arnautov Date: Wed, 30 Sep 2020 16:29:51 +0200 Subject: [PATCH 01/24] [ML] replace swim lane vis --- .../application/explorer/anomaly_timeline.tsx | 2 + .../explorer/swimlane_container.tsx | 185 ++++++++++++++---- 2 files changed, 151 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 45dada84de20a..152180d19122a 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -211,6 +211,7 @@ export const AnomalyTimeline: FC = React.memo( = React.memo( {viewBySwimlaneOptions.length > 0 && ( void; isLoading: boolean; noDataWarning: string | JSX.Element | null; + id: string; } > = ({ + id, children, onResize, perPage, @@ -66,7 +70,10 @@ export const SwimlaneContainer: FC< ...props }) => { const [chartWidth, setChartWidth] = useState(0); - const wrapperRef = useRef(null); + + const containerHeightRef = useRef(); + /** Amount of rows during the previous render */ + const prevRowsCount = useRef(); const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { @@ -93,6 +100,22 @@ export const SwimlaneContainer: FC< fromPage && perPage; + const CELL_HEIGHT = 30; + + const rowsCount = props?.swimlaneData?.laneLabels?.length ?? 0; + + useEffect(() => { + // to prevent the visualization form jumping during loading + if (!isLoading || rowsCount !== prevRowsCount.current) { + containerHeightRef.current = rowsCount * CELL_HEIGHT + 58; + prevRowsCount.current = rowsCount; + } + }, [rowsCount, isLoading, prevRowsCount.current]); + + const highlightedData = props.selection + ? { x: props.selection.times.map((v) => v * 1000), y: props.selection.lanes } + : undefined; + return ( <> @@ -106,39 +129,129 @@ export const SwimlaneContainer: FC< }} data-test-subj="mlSwimLaneContainer" > - -
- - {showSwimlane && !isLoading && ( - - {(tooltipService) => ( - - )} - - )} - {isLoading && ( - - - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} - /> - )} + + {showSwimlane && !isLoading && ( + + { + const [[cell]] = e; + const payload = { + lanes: [cell.datum.y], + times: [cell.datum.x / 1000, cell.datum.x / 1000], + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }; + props.onCellsSelection(payload); + }} + showLegend={false} + legendPosition="top" + onBrushEnd={(e) => { + const payload = { + lanes: e.y, + times: e.x.map((v) => v / 1000), + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }; + props.onCellsSelection(payload); + }} + brushAxis="both" + xDomain={{ + min: props.swimlaneData.earliest * 1000, + max: props.swimlaneData.latest * 1000, + minInterval: props.swimlaneData.interval * 1000, + }} + /> + ({ ...v, time: v.time * 1000 })) + .filter((v) => v.value > 0)} + xAccessor={(d) => d.time} + yAccessor={(d) => { + return d.laneLabel; + }} + valueAccessor={(d) => { + return d.value; + }} + highlightedData={highlightedData} + valueFormatter={(d) => d.toFixed(0.2)} + xScaleType={'time'} + ySortPredicate="dataIndex" + config={{ + grid: { + cellHeight: { + min: CELL_HEIGHT, + max: CELL_HEIGHT, // 'fill', + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + }, + xAxisLabel: { + // eui color subdued + fill: `#98A2B3`, + formatter: (v) => { + props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); + const a = props.timeBuckets.getScaledDateFormat(); + return moment(v).format(a); + }, + }, + }} + /> + + )} + + {isLoading && ( + + -
+ )} + {!isLoading && !showSwimlane && ( + {noDataWarning}} + /> + )}
{isPaginationVisible && ( From d2c7bff25572fc79093ae592fcc200413112ea8d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 2 Oct 2020 18:14:02 +0200 Subject: [PATCH 02/24] [ML] update swimlane_container, add colors constant --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ff98d7f85dcef..8438cb69d1068 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "21.1.2", + "@elastic/charts": "23.1.0", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 278e8efd2d29e..5b0ce313c2a7d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "21.1.2", + "@elastic/charts": "23.1.0", "@elastic/eui": "29.0.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/yarn.lock b/yarn.lock index 2d72b6d6c3bb6..33d59297c08bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,10 +1218,10 @@ dependencies: "@elastic/apm-rum-core" "^5.7.0" -"@elastic/charts@21.1.2": - version "21.1.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.1.2.tgz#da7e9c1025bf730a738b6ac6d7024d97dd2b5aa2" - integrity sha512-Uri+Xolgii7/mRSarfXTfA6X2JC76ILIxTPO8RlYdI44gzprJfUO7Aw5s8vVQke3x6Cu39a+9B0s6TY4GAaApQ== +"@elastic/charts@23.1.0": + version "23.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.1.0.tgz#343c3c54a308a0a54a6dd3387854ef143e8a7c7f" + integrity sha512-pBXCRSGd8Kzg0r60vjuSoB+ngbEnJQG+kFI7h6BbLFodp3/F1vONVsFvqG58ZBAsCb85/M3s4w0gR7qXNobr3g== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1229,6 +1229,7 @@ d3-array "^1.2.4" d3-collection "^1.0.7" d3-color "^1.4.0" + d3-interpolate "^1.4.0" d3-scale "^1.0.7" d3-shape "^1.3.4" newtype-ts "^0.2.4" From 577342c1360a9eb60641bc6ffce59555d75e0df6 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 2 Oct 2020 18:10:15 +0200 Subject: [PATCH 03/24] [ML] update swimlane_container, add colors constant --- x-pack/plugins/ml/common/constants/anomalies.ts | 9 +++++++++ x-pack/plugins/ml/common/index.ts | 2 +- x-pack/plugins/ml/common/util/anomaly_utils.ts | 14 +++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/anomalies.ts b/x-pack/plugins/ml/common/constants/anomalies.ts index 73a24bc11fe66..2b3501554b8dc 100644 --- a/x-pack/plugins/ml/common/constants/anomalies.ts +++ b/x-pack/plugins/ml/common/constants/anomalies.ts @@ -21,6 +21,15 @@ export enum ANOMALY_THRESHOLD { LOW = 0, } +export const SEVERITY_COLORS = { + CRITICAL: '#fe5050', + MAJOR: '#fba740', + MINOR: '#fdec25', + WARNING: '#8bc8fb', + LOW: '#d2e9f7', + BLANK: '#ffffff', +}; + export const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; export const JOB_ID = 'job_id'; export const PARTITION_FIELD_VALUE = 'partition_field_value'; diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index d808e4277f075..d527a9a9780ad 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -5,6 +5,6 @@ */ export { SearchResponse7 } from './types/es_client'; -export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './constants/anomalies'; +export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { composeValidators, patternValidator } from './util/validators'; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 16802040059a7..12ffe89e3126c 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; -import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../constants/anomalies'; import { AnomalyRecordDoc } from '../types/anomalies'; export interface SeverityType { @@ -168,17 +168,17 @@ export function getSeverityWithLow(normalizedScore: number): SeverityType { // for the supplied normalized anomaly score (a value between 0 and 100). export function getSeverityColor(normalizedScore: number): string { if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { - return '#fe5050'; + return SEVERITY_COLORS.CRITICAL; } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { - return '#fba740'; + return SEVERITY_COLORS.MAJOR; } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { - return '#fdec25'; + return SEVERITY_COLORS.MINOR; } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { - return '#8bc8fb'; + return SEVERITY_COLORS.WARNING; } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { - return '#d2e9f7'; + return SEVERITY_COLORS.LOW; } else { - return '#ffffff'; + return SEVERITY_COLORS.BLANK; } } From 7ade2c3aeb6f04d8b606d51861002d02785d3428 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 2 Oct 2020 18:11:49 +0200 Subject: [PATCH 04/24] [ML] update swimlane_container, add colors constant --- .../explorer/swimlane_container.tsx | 352 +++++++++--------- 1 file changed, 184 insertions(+), 168 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 4cc8092120685..a5f0cfcd17e50 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiText, EuiLoadingChart, @@ -15,36 +15,38 @@ import { } from '@elastic/eui'; import { throttle } from 'lodash'; -import { Chart, Settings, Heatmap } from '@elastic/charts'; +import { + Chart, + Settings, + Heatmap, + HeatmapElementEvent, + HeatmapConfig, + ElementClickListener, +} from '@elastic/charts'; import moment from 'moment'; +import { HeatmapBrushEvent } from '@elastic/charts/dist/chart_types/heatmap/layout/types/config_types'; import { ExplorerSwimlaneProps } from './explorer_swimlane'; import { SwimLanePagination } from './swimlane_pagination'; import { ViewBySwimLaneData } from './explorer_utils'; -import { ANOMALY_THRESHOLD } from '../../../common'; +import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; +import { DeepPartial } from '../../../common/types/common'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ const RESIZE_IGNORED_DIFF_PX = 20; const RESIZE_THROTTLE_TIME_MS = 500; +const CELL_HEIGHT = 30; +const LEGEND_HEIGHT = 70; export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); } /** - * Anomaly swim lane container responsible for handling resizing, pagination and injecting - * tooltip service. - * - * @param children - * @param onResize - * @param perPage - * @param fromPage - * @param swimlaneLimit - * @param onPaginationChange - * @param props - * @constructor + * Anomaly swim lane container responsible for handling resizing, pagination and + * providing swim lane vis with required props. */ export const SwimlaneContainer: FC< Omit & { @@ -71,9 +73,8 @@ export const SwimlaneContainer: FC< }) => { const [chartWidth, setChartWidth] = useState(0); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); - /** Amount of rows during the previous render */ - const prevRowsCount = useRef(); const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { @@ -100,173 +101,188 @@ export const SwimlaneContainer: FC< fromPage && perPage; - const CELL_HEIGHT = 30; - const rowsCount = props?.swimlaneData?.laneLabels?.length ?? 0; + const swimLanePoints = useMemo(() => { + return props.swimlaneData.points + .map((v) => ({ ...v, time: v.time * 1000 })) + .filter((v) => v.value > 0); + }, [props.swimlaneData.points]); + + const containerHeight = useMemo(() => { + // Persists container height during loading to prevent page from jumping + return isLoading ? containerHeightRef.current : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT; + }, [isLoading, rowsCount]); + useEffect(() => { - // to prevent the visualization form jumping during loading - if (!isLoading || rowsCount !== prevRowsCount.current) { - containerHeightRef.current = rowsCount * CELL_HEIGHT + 58; - prevRowsCount.current = rowsCount; + if (!isLoading) { + containerHeightRef.current = containerHeight; } - }, [rowsCount, isLoading, prevRowsCount.current]); + }, [isLoading, containerHeight]); const highlightedData = props.selection ? { x: props.selection.times.map((v) => v * 1000), y: props.selection.lanes } : undefined; + const swimLaneConfig: DeepPartial = useMemo( + () => ({ + onBrushEnd: (e: HeatmapBrushEvent) => { + props.onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT / 2, + max: CELL_HEIGHT, // 'fill', + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + }, + xAxisLabel: { + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); + const a = props.timeBuckets.getScaledDateFormat(); + return moment(v).format(a); + }, + }, + }), + [props.swimlaneType, props.swimlaneData?.fieldName] + ); + + // @ts-ignore + const onElementClick: ElementClickListener = useCallback( + (e: HeatmapElementEvent[]) => { + const cell = e[0][0]; + const startTime = (cell.datum.x as number) / 1000; + const payload = { + lanes: [String(cell.datum.y)], + times: [startTime, startTime + props.swimlaneData.interval], + type: props.swimlaneType, + viewByFieldName: props.swimlaneData.fieldName, + }; + props.onCellsSelection(payload); + }, + [props.swimlaneType, props.swimlaneData?.fieldName, props.swimlaneData?.interval] + ); + + // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly return ( - <> - - {(resizeRef) => ( - { - resizeRef(el); + + {(resizeRef) => ( + + - - {showSwimlane && !isLoading && ( - - { - const [[cell]] = e; - const payload = { - lanes: [cell.datum.y], - times: [cell.datum.x / 1000, cell.datum.x / 1000], - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, - }; - props.onCellsSelection(payload); - }} - showLegend={false} - legendPosition="top" - onBrushEnd={(e) => { - const payload = { - lanes: e.y, - times: e.x.map((v) => v / 1000), - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, - }; - props.onCellsSelection(payload); - }} - brushAxis="both" - xDomain={{ - min: props.swimlaneData.earliest * 1000, - max: props.swimlaneData.latest * 1000, - minInterval: props.swimlaneData.interval * 1000, - }} - /> - ({ ...v, time: v.time * 1000 })) - .filter((v) => v.value > 0)} - xAccessor={(d) => d.time} - yAccessor={(d) => { - return d.laneLabel; - }} - valueAccessor={(d) => { - return d.value; - }} - highlightedData={highlightedData} - valueFormatter={(d) => d.toFixed(0.2)} - xScaleType={'time'} - ySortPredicate="dataIndex" - config={{ - grid: { - cellHeight: { - min: CELL_HEIGHT, - max: CELL_HEIGHT, // 'fill', - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - }, - xAxisLabel: { - // eui color subdued - fill: `#98A2B3`, - formatter: (v) => { - props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); - const a = props.timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - }} - /> - - )} - - {isLoading && ( - - - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} + {showSwimlane && !isLoading && ( + + - )} - + d.toFixed(0.2)} + xScaleType="time" + ySortPredicate="dataIndex" + config={swimLaneConfig} + /> + + )} - {isPaginationVisible && ( - - + - + )} - - )} - - + {!isLoading && !showSwimlane && ( + {noDataWarning}} + /> + )} + + + {isPaginationVisible && ( + + + + )} + + )} + ); }; From c65888df4b6bda773652352317c31c98354b328c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 5 Oct 2020 11:51:48 +0200 Subject: [PATCH 05/24] [ML] unfiltered label for Overall swim lane --- .../explorer/swimlane_container.tsx | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index a5f0cfcd17e50..2de9ed6ca43dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -25,12 +25,15 @@ import { } from '@elastic/charts'; import moment from 'moment'; import { HeatmapBrushEvent } from '@elastic/charts/dist/chart_types/heatmap/layout/types/config_types'; -import { ExplorerSwimlaneProps } from './explorer_swimlane'; +import { i18n } from '@kbn/i18n'; import { SwimLanePagination } from './swimlane_pagination'; -import { ViewBySwimLaneData } from './explorer_utils'; +import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; import { DeepPartial } from '../../../common/types/common'; +import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; +import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; +import { mlEscape } from '../util/string_utils'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -44,12 +47,23 @@ export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); } +export interface ExplorerSwimlaneProps { + filterActive?: boolean; + maskAll?: boolean; + timeBuckets: InstanceType; + swimlaneData: OverallSwimlaneData | ViewBySwimLaneData; + swimlaneType: SwimlaneType; + selection?: AppStateSelectedCells; + onCellsSelection: (payload?: AppStateSelectedCells) => void; + 'data-test-subj'?: string; +} + /** * Anomaly swim lane container responsible for handling resizing, pagination and * providing swim lane vis with required props. */ export const SwimlaneContainer: FC< - Omit & { + ExplorerSwimlaneProps & { onResize: (width: number) => void; fromPage?: number; perPage?: number; @@ -57,11 +71,13 @@ export const SwimlaneContainer: FC< onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void; isLoading: boolean; noDataWarning: string | JSX.Element | null; + /** + * Unique id of the chart + */ id: string; } > = ({ id, - children, onResize, perPage, fromPage, @@ -69,7 +85,13 @@ export const SwimlaneContainer: FC< onPaginationChange, isLoading, noDataWarning, - ...props + filterActive, + swimlaneData, + swimlaneType, + selection, + onCellsSelection, + timeBuckets, + maskAll, }) => { const [chartWidth, setChartWidth] = useState(0); @@ -88,11 +110,7 @@ export const SwimlaneContainer: FC< [chartWidth] ); - const showSwimlane = - props.swimlaneData && - props.swimlaneData.laneLabels && - props.swimlaneData.laneLabels.length > 0 && - props.swimlaneData.points.length > 0; + const showSwimlane = swimlaneData?.laneLabels?.length > 0 && swimlaneData.points.length > 0; const isPaginationVisible = (showSwimlane || isLoading) && @@ -101,13 +119,24 @@ export const SwimlaneContainer: FC< fromPage && perPage; - const rowsCount = props?.swimlaneData?.laneLabels?.length ?? 0; + const rowsCount = swimlaneData?.laneLabels?.length ?? 0; const swimLanePoints = useMemo(() => { - return props.swimlaneData.points - .map((v) => ({ ...v, time: v.time * 1000 })) + const showFilterContext = filterActive === true && swimlaneType === SWIMLANE_TYPE.OVERALL; + + return swimlaneData.points + .map((v) => { + const formatted = { ...v, time: v.time * 1000 }; + if (showFilterContext) { + formatted.laneLabel = i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { + defaultMessage: '{label} (unfiltered)', + values: { label: mlEscape(v.laneLabel) }, + }); + } + return formatted; + }) .filter((v) => v.value > 0); - }, [props.swimlaneData.points]); + }, [swimlaneData.points, filterActive, swimlaneType]); const containerHeight = useMemo(() => { // Persists container height during loading to prevent page from jumping @@ -120,18 +149,30 @@ export const SwimlaneContainer: FC< } }, [isLoading, containerHeight]); - const highlightedData = props.selection - ? { x: props.selection.times.map((v) => v * 1000), y: props.selection.lanes } - : undefined; + const highlightedData = useMemo(() => { + if (!selection) return; + + if ( + (swimlaneType !== selection.type || + (swimlaneData.fieldName !== undefined && + swimlaneData.fieldName !== selection.viewByFieldName)) && + filterActive === false + ) { + // Not this swimlane which was selected. + return; + } + + return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; + }, [selection, swimlaneType]); const swimLaneConfig: DeepPartial = useMemo( () => ({ onBrushEnd: (e: HeatmapBrushEvent) => { - props.onCellsSelection({ + onCellsSelection({ lanes: e.y as string[], times: e.x.map((v) => (v as number) / 1000), - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, }); }, grid: { @@ -166,13 +207,13 @@ export const SwimlaneContainer: FC< // eui color subdued fill: `#98A2B3`, formatter: (v: number) => { - props.timeBuckets.setInterval(`${props.swimlaneData.interval}s`); - const a = props.timeBuckets.getScaledDateFormat(); + timeBuckets.setInterval(`${swimlaneData.interval}s`); + const a = timeBuckets.getScaledDateFormat(); return moment(v).format(a); }, }, }), - [props.swimlaneType, props.swimlaneData?.fieldName] + [swimlaneType, swimlaneData?.fieldName] ); // @ts-ignore @@ -182,13 +223,13 @@ export const SwimlaneContainer: FC< const startTime = (cell.datum.x as number) / 1000; const payload = { lanes: [String(cell.datum.y)], - times: [startTime, startTime + props.swimlaneData.interval], - type: props.swimlaneType, - viewByFieldName: props.swimlaneData.fieldName, + times: [startTime, startTime + swimlaneData.interval], + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, }; - props.onCellsSelection(payload); + onCellsSelection(payload); }, - [props.swimlaneType, props.swimlaneData?.fieldName, props.swimlaneData?.interval] + [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] ); // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly @@ -217,9 +258,9 @@ export const SwimlaneContainer: FC< showLegend legendPosition="top" xDomain={{ - min: props.swimlaneData.earliest * 1000, - max: props.swimlaneData.latest * 1000, - minInterval: props.swimlaneData.interval * 1000, + min: swimlaneData.earliest * 1000, + max: swimlaneData.latest * 1000, + minInterval: swimlaneData.interval * 1000, }} /> Date: Mon, 5 Oct 2020 14:00:15 +0200 Subject: [PATCH 06/24] [ML] tooltip content --- .../chart_tooltip/_chart_tooltip.scss | 1 - .../chart_tooltip/chart_tooltip.tsx | 94 ++++++++++--------- .../explorer/swimlane_container.tsx | 50 ++++++++++ 3 files changed, 102 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss index 46e5d91e1cc83..25be39f3ea2d7 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss @@ -1,7 +1,6 @@ .mlChartTooltip { @include euiToolTipStyle('s'); @include euiFontSizeXS; - position: absolute; padding: 0; transition: opacity $euiAnimSpeedNormal; pointer-events: none; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index 0d94c5ccdfe08..d0ecf65bca443 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -23,6 +23,57 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo return formatter ? formatter(headerData) : headerData.label; }; +/** + * Pure component for rendering the tooltip content with a custom layout across the ML plugin. + */ +export const FormattedTooltip: FC<{ tooltipData: TooltipData }> = ({ tooltipData }) => { + return ( +
+ {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( +
{renderHeader(tooltipData[0])}
+ )} + {tooltipData.length > 1 && ( +
+ {tooltipData + .slice(1) + .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { + const classes = classNames('mlChartTooltip__item', { + // eslint-disable-next-line @typescript-eslint/naming-convention + echTooltip__rowHighlighted: isHighlighted, + }); + + const renderValue = Array.isArray(value) + ? value.map((v) =>
{v}
) + : value; + + return ( +
+ + + {label} + + + {renderValue} + + +
+ ); + })} +
+ )} +
+ ); +}; + +/** + * Tooltip component bundled with the {@link ChartTooltipService} + */ const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) => { const [tooltipData, setData] = useState([]); const refCallback = useRef(); @@ -57,50 +108,9 @@ const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) =
- {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( -
{renderHeader(tooltipData[0])}
- )} - {tooltipData.length > 1 && ( -
- {tooltipData - .slice(1) - .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { - const classes = classNames('mlChartTooltip__item', { - // eslint-disable-next-line @typescript-eslint/naming-convention - echTooltip__rowHighlighted: isHighlighted, - }); - - const renderValue = Array.isArray(value) - ? value.map((v) =>
{v}
) - : value; - - return ( -
- - - {label} - - - {renderValue} - - -
- ); - })} -
- )} +
); }) as TooltipTriggerProps['tooltip'], diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 2de9ed6ca43dc..13753a9a7f503 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -22,11 +22,13 @@ import { HeatmapElementEvent, HeatmapConfig, ElementClickListener, + TooltipValue, } from '@elastic/charts'; import moment from 'moment'; import { HeatmapBrushEvent } from '@elastic/charts/dist/chart_types/heatmap/layout/types/config_types'; import { i18n } from '@kbn/i18n'; +import { TooltipSettings } from '@elastic/charts/dist/specs/settings'; import { SwimLanePagination } from './swimlane_pagination'; import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; @@ -34,6 +36,8 @@ import { DeepPartial } from '../../../common/types/common'; import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; +import { FormattedTooltip } from '../components/chart_tooltip/chart_tooltip'; +import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -47,6 +51,44 @@ export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); } +/** + * Provides a custom tooltip for the anomaly swim lane chart. + */ +const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => ({ values }) => { + const [value] = values; + const [laneLabel, date] = value.label.split(' - '); + + // Display date using same format as Kibana visualizations. + const formattedDate = formatHumanReadableDateTime(new Date(date).getTime()); + const tooltipData: TooltipValue[] = [{ label: formattedDate } as TooltipValue]; + + if (fieldName !== undefined) { + tooltipData.push({ + label: fieldName, + value: laneLabel, + // @ts-ignore + seriesIdentifier: { + key: laneLabel, + }, + valueAccessor: 'fieldName', + }); + } + tooltipData.push({ + label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { + defaultMessage: 'Max anomaly score', + }), + value: value.formattedValue, + color: value.color, + // @ts-ignore + seriesIdentifier: { + key: laneLabel, + }, + valueAccessor: 'anomaly_score', + }); + + return ; +}; + export interface ExplorerSwimlaneProps { filterActive?: boolean; maskAll?: boolean; @@ -232,6 +274,13 @@ export const SwimlaneContainer: FC< [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] ); + const tooltipOptions: TooltipSettings = { + placement: 'auto', + fallbackPlacements: ['left'], + boundary: 'chart', + customTooltip: SwimLaneTooltip(swimlaneData.fieldName), + }; + // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly return ( @@ -262,6 +311,7 @@ export const SwimlaneContainer: FC< max: swimlaneData.latest * 1000, minInterval: swimlaneData.interval * 1000, }} + tooltip={tooltipOptions} /> Date: Mon, 5 Oct 2020 15:31:20 +0200 Subject: [PATCH 07/24] [ML] fix styles, override legend styles --- .../plugins/ml/common/util/anomaly_utils.ts | 7 + .../application/explorer/_explorer.scss | 15 ++ .../explorer/swimlane_container.tsx | 214 ++++++++++-------- 3 files changed, 136 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 12ffe89e3126c..633bc5bd47362 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -109,6 +109,13 @@ function getSeverityTypes() { }); } +/** + * Return formatted severity score. + */ +export function getFormattedSeverityScore(score: number) { + return score < 1 ? '< 1' : score.toFixed(2); +} + // Returns a severity label (one of critical, major, minor, warning or unknown) // for the supplied normalized anomaly score (a value between 0 and 100). export function getSeverity(normalizedScore: number): SeverityType { diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 63c471e66c49a..4588a58a11c63 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -335,3 +335,18 @@ $borderRadius: $euiBorderRadius / 2; } } } + + +.mlSwimLaneContainer { + /* Override legend styles */ + .echLegendListContainer { + height: 34px !important; + } + + .echLegendList { + margin-left: 170px !important; + display: flex !important; + justify-content: space-between !important; + flex-wrap: nowrap; + } +} diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 13753a9a7f503..29e97a62982bd 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -38,6 +38,9 @@ import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; import { FormattedTooltip } from '../components/chart_tooltip/chart_tooltip'; import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; +import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; + +import './_explorer.scss'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -152,7 +155,7 @@ export const SwimlaneContainer: FC< [chartWidth] ); - const showSwimlane = swimlaneData?.laneLabels?.length > 0 && swimlaneData.points.length > 0; + const showSwimlane = swimlaneData?.laneLabels?.length > 0 && swimlaneData?.points.length > 0; const isPaginationVisible = (showSwimlane || isLoading) && @@ -166,6 +169,10 @@ export const SwimlaneContainer: FC< const swimLanePoints = useMemo(() => { const showFilterContext = filterActive === true && swimlaneType === SWIMLANE_TYPE.OVERALL; + if (!swimlaneData?.points) { + return []; + } + return swimlaneData.points .map((v) => { const formatted = { ...v, time: v.time * 1000 }; @@ -178,7 +185,7 @@ export const SwimlaneContainer: FC< return formatted; }) .filter((v) => v.value > 0); - }, [swimlaneData.points, filterActive, swimlaneType]); + }, [swimlaneData?.points, filterActive, swimlaneType]); const containerHeight = useMemo(() => { // Persists container height during loading to prevent page from jumping @@ -208,54 +215,57 @@ export const SwimlaneContainer: FC< }, [selection, swimlaneType]); const swimLaneConfig: DeepPartial = useMemo( - () => ({ - onBrushEnd: (e: HeatmapBrushEvent) => { - onCellsSelection({ - lanes: e.y as string[], - times: e.x.map((v) => (v as number) / 1000), - type: swimlaneType, - viewByFieldName: swimlaneData.fieldName, - }); - }, - grid: { - cellHeight: { - min: CELL_HEIGHT / 2, - max: CELL_HEIGHT, // 'fill', - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - }, - xAxisLabel: { - // eui color subdued - fill: `#98A2B3`, - formatter: (v: number) => { - timeBuckets.setInterval(`${swimlaneData.interval}s`); - const a = timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - }), - [swimlaneType, swimlaneData?.fieldName] + () => + showSwimlane + ? { + onBrushEnd: (e: HeatmapBrushEvent) => { + onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT / 2, + max: CELL_HEIGHT, // 'fill', + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + }, + xAxisLabel: { + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + timeBuckets.setInterval(`${swimlaneData.interval}s`); + const a = timeBuckets.getScaledDateFormat(); + return moment(v).format(a); + }, + }, + } + : {}, + [showSwimlane, swimlaneType, swimlaneData?.fieldName] ); // @ts-ignore @@ -274,12 +284,15 @@ export const SwimlaneContainer: FC< [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] ); - const tooltipOptions: TooltipSettings = { - placement: 'auto', - fallbackPlacements: ['left'], - boundary: 'chart', - customTooltip: SwimLaneTooltip(swimlaneData.fieldName), - }; + const tooltipOptions: TooltipSettings = useMemo( + () => ({ + placement: 'auto', + fallbackPlacements: ['left'], + boundary: 'chart', + customTooltip: SwimLaneTooltip(swimlaneData?.fieldName), + }), + [swimlaneData?.fieldName] + ); // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly return ( @@ -296,53 +309,54 @@ export const SwimlaneContainer: FC< style={{ width: '100%', overflowY: 'auto', - height: `${containerHeight}px`, }} grow={false} > - {showSwimlane && !isLoading && ( - - - d.toFixed(0.2)} - xScaleType="time" - ySortPredicate="dataIndex" - config={swimLaneConfig} - /> - - )} +
+ {showSwimlane && !isLoading && ( + + + + + )} +
{isLoading && ( From 42e70ba89b9b25fa4a6c71aa54d3d767ace684d8 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 5 Oct 2020 15:37:39 +0200 Subject: [PATCH 08/24] [ML] hide timeline for overall swimlane on the Anomaly Explorer page --- .../ml/public/application/explorer/anomaly_timeline.tsx | 1 + .../ml/public/application/explorer/swimlane_container.tsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 152180d19122a..65ad24026d615 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -223,6 +223,7 @@ export const AnomalyTimeline: FC = React.memo( onResize={explorerService.setSwimlaneContainerWidth} isLoading={loading} noDataWarning={} + showTimeline={false} /> diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 29e97a62982bd..5f125ac6e5d42 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -120,6 +120,10 @@ export const SwimlaneContainer: FC< * Unique id of the chart */ id: string; + /** + * Enables/disables timeline on the X-axis. + */ + showTimeline?: boolean; } > = ({ id, @@ -137,6 +141,7 @@ export const SwimlaneContainer: FC< onCellsSelection, timeBuckets, maskAll, + showTimeline = true, }) => { const [chartWidth, setChartWidth] = useState(0); @@ -255,6 +260,7 @@ export const SwimlaneContainer: FC< padding: 8, }, xAxisLabel: { + visible: showTimeline, // eui color subdued fill: `#98A2B3`, formatter: (v: number) => { From 1bc52a2313605fe5d8fe5922ae33c82f7aa22588 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 5 Oct 2020 15:44:44 +0200 Subject: [PATCH 09/24] [ML] remove explorer_swimlane component --- .../application/explorer/_explorer.scss | 287 ------- .../explorer/explorer_swimlane.test.tsx | 126 --- .../explorer/explorer_swimlane.tsx | 758 ------------------ 3 files changed, 1171 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_swimlane.test.tsx delete mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 4588a58a11c63..1e2b0593081a9 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -1,48 +1,10 @@ $borderRadius: $euiBorderRadius / 2; -.ml-swimlane-selector { - visibility: hidden; -} - .ml-explorer { width: 100%; display: inline-block; color: $euiColorDarkShade; - .visError { - h4 { - margin-top: 50px; - } - } - - .no-results-container { - text-align: center; - font-size: $euiFontSizeL; - - // SASSTODO: Use a proper calc - padding-top: 60px; - - .no-results { - background-color: $euiFocusBackgroundColor; - padding: $euiSize; - border-radius: $euiBorderRadius; - display: inline-block; - - // SASSTODO: Make a proper selector - i { - color: $euiColorPrimary; - margin-right: $euiSizeXS; - } - - - // SASSTODO: Make a proper selector - div:nth-child(2) { - margin-top: $euiSizeXS; - font-size: $euiFontSizeXS; - } - } - } - .mlAnomalyExplorer__filterBar { padding-right: $euiSize; padding-left: $euiSize; @@ -79,23 +41,6 @@ $borderRadius: $euiBorderRadius / 2; } } - .ml-controls { - padding-bottom: $euiSizeS; - - // SASSTODO: Make a proper selector - label { - font-size: $euiFontSizeXS; - padding: $euiSizeXS; - padding-top: 0; - } - - .kuiButtonGroup { - padding: 0px $euiSizeXS 0px 0px; - position: relative; - display: inline-block; - } - } - .ml-anomalies-controls { padding-top: $euiSizeXS; @@ -103,240 +48,8 @@ $borderRadius: $euiBorderRadius / 2; padding-top: $euiSizeL; } } - - // SASSTODO: This entire selector needs to be rewritten. - // It looks extremely brittle with very specific sizing units - .mlExplorerSwimlane { - user-select: none; - padding: 0; - - line.gridLine { - stroke: $euiBorderColor; - fill: none; - shape-rendering: crispEdges; - stroke-width: 1px; - } - - rect.gridCell { - shape-rendering: crispEdges; - } - - rect.hovered { - stroke: $euiColorDarkShade; - stroke-width: 2px; - } - - text.laneLabel { - font-size: 9pt; - font-family: $euiFontFamily; - fill: $euiColorDarkShade; - } - - text.timeLabel { - font-size: 8pt; - font-family: $euiFontFamily; - fill: $euiColorDarkShade; - } - } } -/* using !important in the following rule because other related legacy rules have more specifity. */ -.mlDragselectDragging { - - .sl-cell-inner, - .sl-cell-inner-dragselect { - opacity: 0.6 !important; - } -} - -/* using !important in the following rule because other related legacy rules have more specifity. */ -.mlHideRangeSelection { - div.ml-swimlanes { - div.lane { - div.cells-container { - .sl-cell.ds-selected { - - .sl-cell-inner, - .sl-cell-inner-dragselect { - border-width: 0px !important; - opacity: 1 !important; - } - - .sl-cell-inner.sl-cell-inner-selected { - border-width: $euiSizeXS / 2 !important; - } - - .sl-cell-inner.sl-cell-inner-masked { - opacity: 0.6 !important; - } - } - } - } - } -} - -.ml-swimlanes { - margin: 0px 0px 0px 10px; - - div.cells-marker-container { - margin-left: 176px; - height: 22px; - white-space: nowrap; - - // background-color: #CCC; - .sl-cell { - height: 10px; - display: inline-block; - vertical-align: top; - margin-top: 16px; - text-align: center; - visibility: hidden; - cursor: default; - - i { - color: $euiColorDarkShade; - } - } - - .sl-cell-hover { - visibility: visible; - - i { - display: block; - margin-top: -6px; - } - } - - .sl-cell-active-hover { - visibility: visible; - - .floating-time-label { - display: inline-block; - } - } - } - - div.lane { - height: 30px; - border-bottom: 0px; - border-radius: $borderRadius; - white-space: nowrap; - - &:not(:first-child) { - margin-top: -1px; - } - - div.lane-label { - display: inline-block; - font-size: $euiFontSizeXS; - height: 30px; - text-align: right; - vertical-align: middle; - border-radius: $borderRadius; - padding-right: 5px; - margin-right: 5px; - border: 1px solid transparent; - overflow: hidden; - text-overflow: ellipsis; - } - - div.lane-label.lane-label-masked { - opacity: 0.3; - } - - div.cells-container { - border: $euiBorderThin; - border-right: 0px; - display: inline-block; - height: 30px; - vertical-align: middle; - background-color: $euiColorEmptyShade; - - .sl-cell { - color: $euiColorEmptyShade; - cursor: default; - display: inline-block; - height: 29px; - border-right: $euiBorderThin; - vertical-align: top; - position: relative; - - .sl-cell-inner, - .sl-cell-inner-dragselect { - height: 26px; - margin: 1px; - border-radius: $borderRadius; - text-align: center; - } - - .sl-cell-inner.sl-cell-inner-masked { - opacity: 0.2; - } - - .sl-cell-inner.sl-cell-inner-selected, - .sl-cell-inner-dragselect.sl-cell-inner-selected { - border: 2px solid $euiColorDarkShade; - } - - .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, - .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { - border: 2px solid $euiColorFullShade; - opacity: 0.4; - } - } - - .sl-cell:hover { - .sl-cell-inner { - opacity: 0.8; - cursor: pointer; - } - } - - .sl-cell.ds-selected { - - .sl-cell-inner, - .sl-cell-inner-dragselect { - border: 2px solid $euiColorDarkShade; - border-radius: $borderRadius; - opacity: 1; - } - } - - } - } - - div.lane:last-child { - div.cells-container { - .sl-cell { - border-bottom: $euiBorderThin; - } - } - } - - .time-tick-labels { - height: 25px; - margin-top: $euiSizeXS / 2; - margin-left: 175px; - - /* hide d3's domain line */ - path.domain { - display: none; - } - - /* hide d3's tick line */ - g.tick line { - display: none; - } - - /* override d3's default tick styles */ - g.tick text { - font-size: 11px; - fill: $euiColorMediumShade; - } - } -} - - .mlSwimLaneContainer { /* Override legend styles */ .echLegendListContainer { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.test.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.test.tsx deleted file mode 100644 index f7ae5f232999e..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.test.tsx +++ /dev/null @@ -1,126 +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. - */ - -import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; - -import moment from 'moment-timezone'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; - -import { ExplorerSwimlane } from './explorer_swimlane'; -import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; -import { ChartTooltipService } from '../components/chart_tooltip'; -import { OverallSwimlaneData } from './explorer_utils'; - -jest.mock('d3', () => { - const original = jest.requireActual('d3'); - - return { - ...original, - transform: jest.fn().mockReturnValue({ - translate: jest.fn().mockReturnValue(0), - }), - }; -}); - -jest.mock('@elastic/eui', () => { - return { - htmlIdGenerator: jest.fn(() => { - return jest.fn(() => { - return 'test-gen-id'; - }); - }), - }; -}); - -function getExplorerSwimlaneMocks() { - const swimlaneData = ({ laneLabels: [] } as unknown) as OverallSwimlaneData; - - const timeBuckets = ({ - setInterval: jest.fn(), - getScaledDateFormat: jest.fn(), - } as unknown) as InstanceType; - - const tooltipService = ({ - show: jest.fn(), - hide: jest.fn(), - } as unknown) as ChartTooltipService; - - return { - timeBuckets, - swimlaneData, - tooltipService, - parentRef: {} as React.RefObject, - }; -} - -const mockChartWidth = 800; - -describe('ExplorerSwimlane', () => { - const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 } as DOMRect; - // @ts-ignore - const originalGetBBox = SVGElement.prototype.getBBox; - beforeEach(() => { - moment.tz.setDefault('UTC'); - // @ts-ignore - SVGElement.prototype.getBBox = () => mockedGetBBox; - }); - afterEach(() => { - moment.tz.setDefault('Browser'); - // @ts-ignore - SVGElement.prototype.getBBox = originalGetBBox; - }); - - test('Minimal initialization', () => { - const mocks = getExplorerSwimlaneMocks(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.html()).toBe( - '
' - ); - - // test calls to mock functions - // @ts-ignore - expect(mocks.timeBuckets.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); - // @ts-ignore - expect(mocks.timeBuckets.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); - }); - - test('Overall swimlane', () => { - const mocks = getExplorerSwimlaneMocks(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.html()).toMatchSnapshot(); - - // test calls to mock functions - // @ts-ignore - expect(mocks.timeBuckets.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); - // @ts-ignore - expect(mocks.timeBuckets.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); - }); -}); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx deleted file mode 100644 index 569709d648b3c..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ /dev/null @@ -1,758 +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. - */ - -/* - * React component for rendering Explorer dashboard swimlanes. - */ - -import React from 'react'; -import './_explorer.scss'; -import { isEqual, uniq, get } from 'lodash'; -import d3 from 'd3'; -import moment from 'moment'; -import DragSelect from 'dragselect'; - -import { i18n } from '@kbn/i18n'; -import { Subject, Subscription } from 'rxjs'; -import { TooltipValue } from '@elastic/charts'; -import { htmlIdGenerator } from '@elastic/eui'; -import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; -import { numTicksForDateFormat } from '../util/chart_utils'; -import { getSeverityColor } from '../../../common/util/anomaly_utils'; -import { mlEscape } from '../util/string_utils'; -import { ALLOW_CELL_RANGE_SELECTION } from './explorer_dashboard_service'; -import { DRAG_SELECT_ACTION, SwimlaneType } from './explorer_constants'; -import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; -import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; -import { - ChartTooltipService, - ChartTooltipValue, -} from '../components/chart_tooltip/chart_tooltip_service'; -import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; - -const SCSS = { - mlDragselectDragging: 'mlDragselectDragging', - mlHideRangeSelection: 'mlHideRangeSelection', -}; - -interface NodeWithData extends Node { - __clickData__: { - time: number; - bucketScore: number; - laneLabel: string; - swimlaneType: string; - }; -} - -interface SelectedData { - bucketScore: number; - laneLabels: string[]; - times: number[]; -} - -export interface ExplorerSwimlaneProps { - chartWidth: number; - filterActive?: boolean; - maskAll?: boolean; - timeBuckets: InstanceType; - swimlaneData: OverallSwimlaneData | ViewBySwimLaneData; - swimlaneType: SwimlaneType; - selection?: AppStateSelectedCells; - onCellsSelection: (payload?: AppStateSelectedCells) => void; - tooltipService: ChartTooltipService; - 'data-test-subj'?: string; - /** - * We need to be aware of the parent element in order to set - * the height so the swim lane widget doesn't jump during loading - * or page changes. - */ - parentRef: React.RefObject; -} - -export class ExplorerSwimlane extends React.Component { - // Since this component is mostly rendered using d3 and cellMouseoverActive is only - // relevant for d3 based interaction, we don't manage this using React's state - // and intentionally circumvent the component lifecycle when updating it. - cellMouseoverActive = true; - - selection: AppStateSelectedCells | undefined = undefined; - - dragSelectSubscriber: Subscription | null = null; - - rootNode = React.createRef(); - - isSwimlaneSelectActive = false; - // make sure dragSelect is only available if the mouse pointer is actually over a swimlane - disableDragSelectOnMouseLeave = true; - - dragSelect$ = new Subject<{ - action: typeof DRAG_SELECT_ACTION[keyof typeof DRAG_SELECT_ACTION]; - elements?: any[]; - }>(); - - /** - * Unique id for swim lane instance - */ - rootNodeId = htmlIdGenerator()(); - - /** - * Initialize drag select instance - */ - dragSelect = new DragSelect({ - selectorClass: 'ml-swimlane-selector', - selectables: document.querySelectorAll(`#${this.rootNodeId} .sl-cell`), - callback: (elements) => { - if (elements.length > 1 && !ALLOW_CELL_RANGE_SELECTION) { - elements = [elements[0]]; - } - - if (elements.length > 0) { - this.dragSelect$.next({ - action: DRAG_SELECT_ACTION.NEW_SELECTION, - elements, - }); - } - - this.disableDragSelectOnMouseLeave = true; - }, - onDragStart: (e) => { - // make sure we don't trigger text selection on label - e.preventDefault(); - // clear previous selection - this.clearSelection(); - let target = e.target as HTMLElement; - while (target && target !== document.body && !target.classList.contains('sl-cell')) { - target = target.parentNode as HTMLElement; - } - if (ALLOW_CELL_RANGE_SELECTION && target !== document.body) { - this.dragSelect$.next({ - action: DRAG_SELECT_ACTION.DRAG_START, - }); - this.disableDragSelectOnMouseLeave = false; - } - }, - onElementSelect: () => { - if (ALLOW_CELL_RANGE_SELECTION) { - this.dragSelect$.next({ - action: DRAG_SELECT_ACTION.ELEMENT_SELECT, - }); - } - }, - }); - - componentDidMount() { - // property for data comparison to be able to filter - // consecutive click events with the same data. - let previousSelectedData: any = null; - - // Listen for dragSelect events - this.dragSelectSubscriber = this.dragSelect$.subscribe(({ action, elements = [] }) => { - const element = d3.select(this.rootNode.current!.parentNode!); - const { swimlaneType } = this.props; - - if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { - element.classed(SCSS.mlDragselectDragging, false); - const firstSelectedCell = (d3.select(elements[0]).node() as NodeWithData).__clickData__; - - if ( - typeof firstSelectedCell !== 'undefined' && - swimlaneType === firstSelectedCell.swimlaneType - ) { - const selectedData: SelectedData = elements.reduce( - (d, e) => { - const cell = (d3.select(e).node() as NodeWithData).__clickData__; - d.bucketScore = Math.max(d.bucketScore, cell.bucketScore); - d.laneLabels.push(cell.laneLabel); - d.times.push(cell.time); - return d; - }, - { - bucketScore: 0, - laneLabels: [], - times: [], - } - ); - - selectedData.laneLabels = uniq(selectedData.laneLabels); - selectedData.times = uniq(selectedData.times); - if (isEqual(selectedData, previousSelectedData) === false) { - // If no cells containing anomalies have been selected, - // immediately clear the selection, otherwise trigger - // a reload with the updated selected cells. - if (selectedData.bucketScore === 0) { - elements.map((e) => d3.select(e).classed('ds-selected', false)); - this.selectCell([], selectedData); - previousSelectedData = null; - } else { - this.selectCell(elements, selectedData); - previousSelectedData = selectedData; - } - } - } - - this.cellMouseoverActive = true; - } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { - element.classed(SCSS.mlDragselectDragging, true); - } else if (action === DRAG_SELECT_ACTION.DRAG_START) { - previousSelectedData = null; - this.cellMouseoverActive = false; - this.props.tooltipService.hide(); - } - }); - - this.renderSwimlane(); - - this.dragSelect.stop(); - } - - componentDidUpdate() { - this.renderSwimlane(); - } - - componentWillUnmount() { - this.dragSelectSubscriber!.unsubscribe(); - // Remove selector element from DOM - this.dragSelect.selector.remove(); - // removes all mousedown event handlers - this.dragSelect.stop(true); - } - - selectCell(cellsToSelect: any[], { laneLabels, bucketScore, times }: SelectedData) { - const { selection, swimlaneData, swimlaneType } = this.props; - - let triggerNewSelection = false; - - if (cellsToSelect.length > 1 || bucketScore > 0) { - triggerNewSelection = true; - } - - // Check if the same cells were selected again, if so clear the selection, - // otherwise activate the new selection. The two objects are built for - // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" - // since it also includes the "viewBy" attribute which might differ depending - // on whether the overall or viewby swimlane was selected. - const oldSelection = { - selectedType: selection && selection.type, - selectedLanes: selection && selection.lanes, - selectedTimes: selection && selection.times, - }; - - const newSelection = { - selectedType: swimlaneType, - selectedLanes: laneLabels, - selectedTimes: d3.extent(times), - }; - - if (isEqual(oldSelection, newSelection)) { - triggerNewSelection = false; - } - - if (triggerNewSelection === false) { - this.swimLaneSelectionCompleted(); - return; - } - - const selectedCells = { - viewByFieldName: swimlaneData.fieldName, - lanes: laneLabels, - times: d3.extent(times), - type: swimlaneType, - }; - this.swimLaneSelectionCompleted(selectedCells); - } - - /** - * Highlights DOM elements of the swim lane cells - */ - highlightSwimLaneCells(selection: AppStateSelectedCells | undefined) { - const element = d3.select(this.rootNode.current!.parentNode!); - - const { swimlaneType, swimlaneData, filterActive, maskAll } = this.props; - - const { laneLabels: lanes, earliest: startTime, latest: endTime } = swimlaneData; - - // Check for selection and reselect the corresponding swimlane cell - // if the time range and lane label are still in view. - const selectionState = selection; - const selectedType = get(selectionState, 'type', undefined); - const selectionViewByFieldName = get(selectionState, 'viewByFieldName', ''); - - // If a selection was done in the other swimlane, add the "masked" classes - // to de-emphasize the swimlane cells. - if (swimlaneType !== selectedType && selectedType !== undefined) { - element.selectAll('.lane-label').classed('lane-label-masked', true); - element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); - } - - const cellsToSelect: Node[] = []; - const selectedLanes = get(selectionState, 'lanes', []); - const selectedTimes = get(selectionState, 'times', []); - const selectedTimeExtent = d3.extent(selectedTimes); - - if ( - (swimlaneType !== selectedType || - (swimlaneData.fieldName !== undefined && - swimlaneData.fieldName !== selectionViewByFieldName)) && - filterActive === false - ) { - // Not this swimlane which was selected. - return; - } - - selectedLanes.forEach((selectedLane) => { - if ( - lanes.indexOf(selectedLane) > -1 && - selectedTimeExtent[0] >= startTime && - selectedTimeExtent[1] <= endTime - ) { - // Locate matching cell - look for exact time, otherwise closest before. - const laneCells = element.selectAll(`div[data-lane-label="${mlEscape(selectedLane)}"]`); - - laneCells.each(function (this: HTMLElement) { - const cell = d3.select(this); - const cellTime = parseInt(cell.attr('data-time'), 10); - if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { - cellsToSelect.push(cell.node()); - } - }); - } - }); - - const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { - return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); - }, 0); - - const selectedCellTimes = cellsToSelect.map((e) => { - return (d3.select(e).node() as NodeWithData).__clickData__.time; - }); - - if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - this.highlightSelection(cellsToSelect, selectedLanes, selectedCellTimes); - } else if (filterActive === true) { - this.maskIrrelevantSwimlanes(Boolean(maskAll)); - } else { - this.clearSelection(); - } - - // cache selection to prevent rerenders - this.selection = selection; - } - - highlightSelection(cellsToSelect: Node[], laneLabels: string[], times: number[]) { - // This selects the embeddable container - const wrapper = d3.select(`#${this.rootNodeId}`); - - wrapper.selectAll('.lane-label').classed('lane-label-masked', true); - wrapper - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - wrapper - .selectAll( - '.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected' - ) - .classed('sl-cell-inner-selected', false); - - d3.selectAll(cellsToSelect) - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', false) - .classed('sl-cell-inner-selected', true); - - const rootParent = d3.select(this.rootNode.current!.parentNode!); - rootParent.selectAll('.lane-label').classed('lane-label-masked', function (this: HTMLElement) { - return laneLabels.indexOf(d3.select(this).text()) === -1; - }); - } - - /** - * TODO should happen with props instead of imperative check - * @param maskAll - */ - maskIrrelevantSwimlanes(maskAll: boolean) { - if (maskAll === true) { - // This selects both overall and viewby swimlane - const allSwimlanes = d3.selectAll('.mlExplorerSwimlane'); - allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); - allSwimlanes - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } else { - const overallSwimlane = d3.select('.ml-swimlane-overall'); - overallSwimlane.selectAll('.lane-label').classed('lane-label-masked', true); - overallSwimlane - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } - } - - clearSelection() { - // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.mlExplorerSwimlane'); - - wrapper.selectAll('.lane-label').classed('lane-label-masked', false); - wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); - wrapper - .selectAll('.sl-cell-inner.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper - .selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); - } - - renderSwimlane() { - const element = d3.select(this.rootNode.current!.parentNode!); - - // Consider the setting to support to select a range of cells - if (!ALLOW_CELL_RANGE_SELECTION) { - element.classed(SCSS.mlHideRangeSelection, true); - } - - // This getter allows us to fetch the current value in `cellMouseover()`. - // Otherwise it will just refer to the value when `cellMouseover()` was instantiated. - const getCellMouseoverActive = () => this.cellMouseoverActive; - - const { - chartWidth, - filterActive, - timeBuckets, - swimlaneData, - swimlaneType, - selection, - } = this.props; - - const { - laneLabels: lanes, - earliest: startTime, - latest: endTime, - interval: stepSecs, - points, - } = swimlaneData; - - const cellMouseover = ( - target: HTMLElement, - laneLabel: string, - bucketScore: number, - index: number, - time: number - ) => { - if (bucketScore === undefined || getCellMouseoverActive() === false) { - return; - } - - const displayScore = bucketScore > 1 ? parseInt(String(bucketScore), 10) : '< 1'; - - // Display date using same format as Kibana visualizations. - const formattedDate = formatHumanReadableDateTime(time * 1000); - const tooltipData: TooltipValue[] = [{ label: formattedDate } as TooltipValue]; - - if (swimlaneData.fieldName !== undefined) { - tooltipData.push({ - label: swimlaneData.fieldName, - value: laneLabel, - // @ts-ignore - seriesIdentifier: { - key: laneLabel, - }, - valueAccessor: 'fieldName', - }); - } - tooltipData.push({ - label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { - defaultMessage: 'Max anomaly score', - }), - value: displayScore, - color: colorScore(bucketScore), - // @ts-ignore - seriesIdentifier: { - key: laneLabel, - }, - valueAccessor: 'anomaly_score', - }); - - const offsets = target.className === 'sl-cell-inner' ? { x: 6, y: 0 } : { x: 8, y: 1 }; - - this.props.tooltipService.show(tooltipData, target, { - x: target.offsetWidth + offsets.x, - y: 6 + offsets.y, - }); - }; - - function colorScore(value: number): string { - return getSeverityColor(value); - } - - const numBuckets = Math.round((endTime - startTime) / stepSecs); - const cellHeight = 30; - const height = (lanes.length + 1) * cellHeight - 10; - // Set height for the wrapper element - if (this.props.parentRef.current) { - this.props.parentRef.current.style.height = `${height + 20}px`; - } - - const laneLabelWidth = 170; - const swimlanes = element.select('.ml-swimlanes'); - swimlanes.html(''); - - const cellWidth = Math.floor((chartWidth / numBuckets) * 100) / 100; - - const xAxisWidth = cellWidth * numBuckets; - const xAxisScale = d3.time - .scale() - .domain([new Date(startTime * 1000), new Date(endTime * 1000)]) - .range([0, xAxisWidth]); - - // Get the scaled date format to use for x axis tick labels. - timeBuckets.setInterval(`${stepSecs}s`); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - function cellMouseOverFactory(time: number, i: number) { - // Don't use an arrow function here because we need access to `this`, - // which is where d3 supplies a reference to the corresponding DOM element. - return function (this: HTMLElement, lane: string) { - const bucketScore = getBucketScore(lane, time); - if (bucketScore !== 0) { - lane = lane === '' ? EMPTY_FIELD_VALUE_LABEL : lane; - cellMouseover(this, lane, bucketScore, i, time); - } - }; - } - - const cellMouseleave = () => { - this.props.tooltipService.hide(); - }; - - const d3Lanes = swimlanes.selectAll('.lane').data(lanes); - const d3LanesEnter = d3Lanes.enter().append('div').classed('lane', true); - - const that = this; - - d3LanesEnter - .append('div') - .classed('lane-label', true) - .style('width', `${laneLabelWidth}px`) - .html((label: string) => { - const showFilterContext = filterActive === true && label === 'Overall'; - if (showFilterContext) { - return i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { - defaultMessage: '{label} (unfiltered)', - values: { label: mlEscape(label) }, - }); - } else { - return label === '' ? `${EMPTY_FIELD_VALUE_LABEL}` : mlEscape(label); - } - }) - .on('click', () => { - if (selection && typeof selection.lanes !== 'undefined') { - this.swimLaneSelectionCompleted(); - } - }) - .each(function (this: HTMLElement) { - if (swimlaneData.fieldName !== undefined) { - d3.select(this) - .on('mouseover', (value) => { - that.props.tooltipService.show( - [ - { skipHeader: true } as ChartTooltipValue, - { - label: swimlaneData.fieldName!, - value: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, - // @ts-ignore - seriesIdentifier: { key: value }, - valueAccessor: 'fieldName', - }, - ], - this, - { - x: laneLabelWidth, - y: 0, - } - ); - }) - .on('mouseout', () => { - that.props.tooltipService.hide(); - }) - .attr( - 'aria-label', - (value) => `${mlEscape(swimlaneData.fieldName!)}: ${mlEscape(value)}` - ); - } - }); - - const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); - - function getBucketScore(lane: string, time: number): number { - let bucketScore = 0; - const point = points.find((p) => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - if (typeof point !== 'undefined') { - bucketScore = point.value; - } - return bucketScore; - } - - // TODO - mark if zoomed in to bucket width? - let time = startTime; - Array(numBuckets || 0) - .fill(null) - .forEach((v, i) => { - const cell = cellsContainer - .append('div') - .classed('sl-cell', true) - .style('width', `${cellWidth}px`) - .attr('data-lane-label', (label: string) => mlEscape(label)) - .attr('data-time', time) - .attr('data-bucket-score', (lane: string) => { - return getBucketScore(lane, time); - }) - // use a factory here to bind the `time` and `i` values - // of this iteration to the event. - .on('mouseover', cellMouseOverFactory(time, i)) - .on('mouseleave', cellMouseleave) - .each(function (this: NodeWithData, laneLabel: string) { - this.__clickData__ = { - bucketScore: getBucketScore(laneLabel, time), - laneLabel, - swimlaneType, - time, - }; - }); - - // calls itself with each() to get access to lane (= d3 data) - cell.append('div').each(function (this: HTMLElement, lane: string) { - const el = d3.select(this); - - let color = 'none'; - let bucketScore = 0; - - const point = points.find((p) => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - - if (typeof point !== 'undefined') { - bucketScore = point.value; - color = colorScore(bucketScore); - el.classed('sl-cell-inner', true).style('background-color', color); - } else { - el.classed('sl-cell-inner-dragselect', true); - } - }); - - time += stepSecs; - }); - - // ['x-axis'] is just a placeholder so we have an array of 1. - const laneTimes = swimlanes - .selectAll('.time-tick-labels') - .data(['x-axis']) - .enter() - .append('div') - .classed('time-tick-labels', true); - - // height of .time-tick-labels - const svgHeight = 25; - const svg = laneTimes.append('svg').attr('width', chartWidth).attr('height', svgHeight); - - const xAxis = d3.svg - .axis() - .scale(xAxisScale) - .ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) - .tickFormat((tick) => moment(tick).format(xAxisTickFormat)); - - const gAxis = svg.append('g').attr('class', 'x axis').call(xAxis); - - // remove overlapping labels - let overlapCheck = 0; - gAxis.selectAll('g.tick').each(function (this: HTMLElement) { - const tick = d3.select(this); - const xTransform = d3.transform(tick.attr('transform')).translate[0]; - const tickWidth = (tick.select('text').node() as SVGGraphicsElement).getBBox().width; - const xMinOffset = xTransform - tickWidth / 2; - const xMaxOffset = xTransform + tickWidth / 2; - // if the tick label overlaps the previous label - // (or overflows the chart to the left), remove it; - // otherwise pick that label's offset as the new offset to check against - if (xMinOffset < overlapCheck) { - tick.remove(); - } else { - overlapCheck = xTransform + tickWidth / 2; - } - // if the last tick label overflows the chart to the right, remove it - if (xMaxOffset > chartWidth) { - tick.remove(); - } - }); - - this.swimlaneRenderDoneListener(); - - this.highlightSwimLaneCells(selection); - } - - shouldComponentUpdate(nextProps: ExplorerSwimlaneProps) { - return ( - this.props.chartWidth !== nextProps.chartWidth || - !isEqual(this.props.swimlaneData, nextProps.swimlaneData) || - !isEqual(nextProps.selection, this.selection) - ); - } - - /** - * Listener for click events in the swim lane and execute a prop callback. - * @param selectedCellsUpdate - */ - swimLaneSelectionCompleted(selectedCellsUpdate?: AppStateSelectedCells) { - // If selectedCells is an empty object we clear any existing selection, - // otherwise we save the new selection in AppState and update the Explorer. - this.highlightSwimLaneCells(selectedCellsUpdate); - - if (!selectedCellsUpdate) { - this.props.onCellsSelection(); - } else { - this.props.onCellsSelection(selectedCellsUpdate); - } - } - - /** - * Listens to render updates of the swim lanes to update dragSelect - */ - swimlaneRenderDoneListener() { - this.dragSelect.clearSelection(); - this.dragSelect.setSelectables(document.querySelectorAll(`#${this.rootNodeId} .sl-cell`)); - } - - setSwimlaneSelectActive(active: boolean) { - if (this.isSwimlaneSelectActive && !active && this.disableDragSelectOnMouseLeave) { - this.dragSelect.stop(); - this.isSwimlaneSelectActive = active; - return; - } - if (!this.isSwimlaneSelectActive && active) { - this.dragSelect.start(); - this.dragSelect.clearSelection(); - this.dragSelect.setSelectables(document.querySelectorAll(`#${this.rootNodeId} .sl-cell`)); - this.isSwimlaneSelectActive = active; - } - } - - render() { - const { swimlaneType } = this.props; - - return ( -
-
-
- ); - } -} From 0dc7b5b0804c46fecd50e2384d0d722a5e58f495 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 5 Oct 2020 15:48:45 +0200 Subject: [PATCH 10/24] [ML] remove dragselect dependency --- x-pack/package.json | 2 -- yarn.lock | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index ffe1a08855888..36c6c2dee279a 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -73,7 +73,6 @@ "@types/d3-shape": "^1.3.1", "@types/d3-time": "^1.0.10", "@types/d3-time-format": "^2.1.1", - "@types/dragselect": "^1.13.1", "@types/elasticsearch": "^5.0.33", "@types/fancy-log": "^1.3.1", "@types/file-saver": "^2.0.0", @@ -165,7 +164,6 @@ "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-scale": "1.0.7", - "dragselect": "1.13.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", diff --git a/yarn.lock b/yarn.lock index 1430396933165..abbb243046917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3845,11 +3845,6 @@ resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964" integrity sha512-sq+kwx8zA9BSugT9N+Jr8/uWjbHMZ+N/meJEzRyT3gmLq/WMtx/iSIpvdpmBUi/cvXl6Kzpvve8G2ESkabFwmg== -"@types/dragselect@^1.13.1": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@types/dragselect/-/dragselect-1.13.1.tgz#f19b7b41063a7c9d5963194c83c3c364e84d46ee" - integrity sha512-3m0fvSM0cSs0DXvprytV/ZY92hNX3jJuEb/vkdqU+4QMzV2jxYKgBFTuaT2fflqbmfzUqHHIkGP55WIuigElQw== - "@types/ejs@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.0.4.tgz#8851fcdedb96e410fbb24f83b8be6763ef9afa77" @@ -11025,11 +11020,6 @@ downgrade-root@^1.0.0: default-uid "^1.0.0" is-root "^1.0.0" -dragselect@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/dragselect/-/dragselect-1.13.1.tgz#aa4166e1164b51ed5ee0cd89e0c5310a9c35be6a" - integrity sha512-spfUz6/sNnlY4fF/OxPBwaKLa5hVz6V+fq5XhVuD+h47RAkA75TMkfvr4AoWUh5Ufq3V1oIAbfu+sjc9QbewoA== - dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" From 43291f6fc8ee48736de6ed0fd14307df760c06a9 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 09:37:35 +0200 Subject: [PATCH 11/24] [ML] fix types --- .../explorer/swimlane_container.tsx | 52 +++++++++---------- .../embeddable_swim_lane_container.tsx | 1 + 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 5f125ac6e5d42..7c3132a4fd19d 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -20,9 +20,9 @@ import { Settings, Heatmap, HeatmapElementEvent, - HeatmapConfig, ElementClickListener, TooltipValue, + HeatmapSpec, } from '@elastic/charts'; import moment from 'moment'; import { HeatmapBrushEvent } from '@elastic/charts/dist/chart_types/heatmap/layout/types/config_types'; @@ -32,7 +32,6 @@ import { TooltipSettings } from '@elastic/charts/dist/specs/settings'; import { SwimLanePagination } from './swimlane_pagination'; import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../common'; -import { DeepPartial } from '../../../common/types/common'; import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets'; import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; @@ -48,7 +47,8 @@ import './_explorer.scss'; const RESIZE_IGNORED_DIFF_PX = 20; const RESIZE_THROTTLE_TIME_MS = 500; const CELL_HEIGHT = 30; -const LEGEND_HEIGHT = 70; +const LEGEND_HEIGHT = 34; +const Y_AXIS_HEIGHT = 24; export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); @@ -92,7 +92,7 @@ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => return ; }; -export interface ExplorerSwimlaneProps { +export interface SwimlaneProps { filterActive?: boolean; maskAll?: boolean; timeBuckets: InstanceType; @@ -101,31 +101,28 @@ export interface ExplorerSwimlaneProps { selection?: AppStateSelectedCells; onCellsSelection: (payload?: AppStateSelectedCells) => void; 'data-test-subj'?: string; + onResize: (width: number) => void; + fromPage?: number; + perPage?: number; + swimlaneLimit?: number; + onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void; + isLoading: boolean; + noDataWarning: string | JSX.Element | null; + /** + * Unique id of the chart + */ + id: string; + /** + * Enables/disables timeline on the X-axis. + */ + showTimeline?: boolean; } /** * Anomaly swim lane container responsible for handling resizing, pagination and * providing swim lane vis with required props. */ -export const SwimlaneContainer: FC< - ExplorerSwimlaneProps & { - onResize: (width: number) => void; - fromPage?: number; - perPage?: number; - swimlaneLimit?: number; - onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void; - isLoading: boolean; - noDataWarning: string | JSX.Element | null; - /** - * Unique id of the chart - */ - id: string; - /** - * Enables/disables timeline on the X-axis. - */ - showTimeline?: boolean; - } -> = ({ +export const SwimlaneContainer: FC = ({ id, onResize, perPage, @@ -194,7 +191,9 @@ export const SwimlaneContainer: FC< const containerHeight = useMemo(() => { // Persists container height during loading to prevent page from jumping - return isLoading ? containerHeightRef.current : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT; + return isLoading + ? containerHeightRef.current + : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + Y_AXIS_HEIGHT; }, [isLoading, rowsCount]); useEffect(() => { @@ -203,7 +202,7 @@ export const SwimlaneContainer: FC< } }, [isLoading, containerHeight]); - const highlightedData = useMemo(() => { + const highlightedData: HeatmapSpec['highlightedData'] = useMemo(() => { if (!selection) return; if ( @@ -219,7 +218,7 @@ export const SwimlaneContainer: FC< return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; }, [selection, swimlaneType]); - const swimLaneConfig: DeepPartial = useMemo( + const swimLaneConfig: HeatmapSpec['config'] = useMemo( () => showSwimlane ? { @@ -233,7 +232,6 @@ export const SwimlaneContainer: FC< }, grid: { cellHeight: { - min: CELL_HEIGHT / 2, max: CELL_HEIGHT, // 'fill', }, stroke: { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 0291fa1564a2d..d638e2c231468 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -115,6 +115,7 @@ export const EmbeddableSwimLaneContainer: FC = ( data-test-subj="mlAnomalySwimlaneEmbeddableWrapper" > Date: Tue, 6 Oct 2020 10:04:41 +0200 Subject: [PATCH 12/24] [ML] fix tooltips, change mask fill to white --- .../explorer/swimlane_container.tsx | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 7c3132a4fd19d..443b8296973c2 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -58,36 +58,51 @@ export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { * Provides a custom tooltip for the anomaly swim lane chart. */ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => ({ values }) => { - const [value] = values; - const [laneLabel, date] = value.label.split(' - '); + const tooltipData: TooltipValue[] = []; - // Display date using same format as Kibana visualizations. - const formattedDate = formatHumanReadableDateTime(new Date(date).getTime()); - const tooltipData: TooltipValue[] = [{ label: formattedDate } as TooltipValue]; - - if (fieldName !== undefined) { + if (values.length === 1 && fieldName) { + // Y-axis tooltip + const [yAxis] = values; + // @ts-ignore + tooltipData.push({ skipHeader: true }); tooltipData.push({ label: fieldName, - value: laneLabel, + value: yAxis.value, + // @ts-ignore + seriesIdentifier: { + key: yAxis.value, + }, + }); + } else { + // Cell tooltip + const [xAxis, yAxis, cell] = values; + + // Display date using same format as Kibana visualizations. + const formattedDate = formatHumanReadableDateTime(parseInt(xAxis.value, 10)); + tooltipData.push({ label: formattedDate } as TooltipValue); + + if (fieldName !== undefined) { + tooltipData.push({ + label: fieldName, + value: yAxis.value, + // @ts-ignore + seriesIdentifier: { + key: yAxis.value, + }, + }); + } + tooltipData.push({ + label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { + defaultMessage: 'Max anomaly score', + }), + value: cell.formattedValue, + color: cell.color, // @ts-ignore seriesIdentifier: { - key: laneLabel, + key: cell.value, }, - valueAccessor: 'fieldName', }); } - tooltipData.push({ - label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { - defaultMessage: 'Max anomaly score', - }), - value: value.formattedValue, - color: value.color, - // @ts-ignore - seriesIdentifier: { - key: laneLabel, - }, - valueAccessor: 'anomaly_score', - }); return ; }; @@ -267,6 +282,9 @@ export const SwimlaneContainer: FC = ({ return moment(v).format(a); }, }, + brushMask: { + fill: 'rgb(247 247 247 / 50%)', + }, } : {}, [showSwimlane, swimlaneType, swimlaneData?.fieldName] From 54bab97762b3cf36206d97121e59925c44f37a04 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 10:07:58 +0200 Subject: [PATCH 13/24] [ML] fix highlightedData --- .../ml/public/application/explorer/swimlane_container.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 443b8296973c2..e3d7774ba38cb 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -226,12 +226,12 @@ export const SwimlaneContainer: FC = ({ swimlaneData.fieldName !== selection.viewByFieldName)) && filterActive === false ) { - // Not this swimlane which was selected. + // Not this swim lane which was selected. return; } return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; - }, [selection, swimlaneType]); + }, [selection, swimlaneType, swimlaneData.fieldName]); const swimLaneConfig: HeatmapSpec['config'] = useMemo( () => From a7de8bcb60f8db7a94c3adbdd32f2424aaa563f7 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 13:22:11 +0200 Subject: [PATCH 14/24] [ML] maxLegendHeight, fix Y-axis tooltip --- .../application/explorer/swimlane_container.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index e3d7774ba38cb..3e18fd0e8a72a 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -61,7 +61,7 @@ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => const tooltipData: TooltipValue[] = []; if (values.length === 1 && fieldName) { - // Y-axis tooltip + // Y-axis tooltip for viewBy swim lane const [yAxis] = values; // @ts-ignore tooltipData.push({ skipHeader: true }); @@ -73,7 +73,7 @@ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => key: yAxis.value, }, }); - } else { + } else if (values.length === 3) { // Cell tooltip const [xAxis, yAxis, cell] = values; @@ -208,8 +208,8 @@ export const SwimlaneContainer: FC = ({ // Persists container height during loading to prevent page from jumping return isLoading ? containerHeightRef.current - : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + Y_AXIS_HEIGHT; - }, [isLoading, rowsCount]); + : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + (showTimeline ? Y_AXIS_HEIGHT : 0); + }, [isLoading, rowsCount, showTimeline]); useEffect(() => { if (!isLoading) { @@ -218,11 +218,11 @@ export const SwimlaneContainer: FC = ({ }, [isLoading, containerHeight]); const highlightedData: HeatmapSpec['highlightedData'] = useMemo(() => { - if (!selection) return; + if (!selection || !swimlaneData) return; if ( (swimlaneType !== selection.type || - (swimlaneData.fieldName !== undefined && + (swimlaneData?.fieldName !== undefined && swimlaneData.fieldName !== selection.viewByFieldName)) && filterActive === false ) { @@ -231,7 +231,7 @@ export const SwimlaneContainer: FC = ({ } return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; - }, [selection, swimlaneType, swimlaneData.fieldName]); + }, [selection, swimlaneData, swimlaneType]); const swimLaneConfig: HeatmapSpec['config'] = useMemo( () => @@ -247,6 +247,7 @@ export const SwimlaneContainer: FC = ({ }, grid: { cellHeight: { + min: CELL_HEIGHT, max: CELL_HEIGHT, // 'fill', }, stroke: { @@ -285,6 +286,7 @@ export const SwimlaneContainer: FC = ({ brushMask: { fill: 'rgb(247 247 247 / 50%)', }, + maxLegendHeight: LEGEND_HEIGHT, } : {}, [showSwimlane, swimlaneType, swimlaneData?.fieldName] From 622fbc809d5cafe497c872286cf5e947d44be837 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 13:36:32 +0200 Subject: [PATCH 15/24] [ML] clear selection --- .../public/application/explorer/anomaly_timeline.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 65ad24026d615..ab722bf0cd1d9 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -18,6 +18,7 @@ import { EuiTitle, EuiSpacer, EuiContextMenuItem, + EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -156,6 +157,16 @@ export const AnomalyTimeline: FC = React.memo( /> + {selectedCells ? ( + + + + + + ) : null}
{viewByLoadedForTimeFormatted && ( From a97dfad259b82dd315a39290042fc19d65ccb6ce Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 13:40:10 +0200 Subject: [PATCH 16/24] [ML] dataTestSubj --- .../ml/public/application/explorer/swimlane_container.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 3e18fd0e8a72a..c67dc8650a071 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -154,6 +154,7 @@ export const SwimlaneContainer: FC = ({ timeBuckets, maskAll, showTimeline = true, + 'data-test-subj': dataTestSubj, }) => { const [chartWidth, setChartWidth] = useState(0); @@ -336,7 +337,7 @@ export const SwimlaneContainer: FC = ({ }} grow={false} > -
+
{showSwimlane && !isLoading && ( Date: Tue, 6 Oct 2020 13:43:37 +0200 Subject: [PATCH 17/24] [ML] remove jest snapshot for explorer_swimlane --- .../explorer/__snapshots__/explorer_swimlane.test.tsx.snap | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap diff --git a/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap b/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap deleted file mode 100644 index 4adaac1319d53..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
2017-02-07T00:00:00Z2017-02-07T00:30:00Z2017-02-07T01:00:00Z2017-02-07T01:30:00Z2017-02-07T02:00:00Z2017-02-07T02:30:00Z2017-02-07T03:00:00Z2017-02-07T03:30:00Z2017-02-07T04:00:00Z2017-02-07T04:30:00Z2017-02-07T05:00:00Z2017-02-07T05:30:00Z2017-02-07T06:00:00Z2017-02-07T06:30:00Z2017-02-07T07:00:00Z2017-02-07T07:30:00Z2017-02-07T08:00:00Z2017-02-07T08:30:00Z2017-02-07T09:00:00Z2017-02-07T09:30:00Z2017-02-07T10:00:00Z2017-02-07T10:30:00Z2017-02-07T11:00:00Z2017-02-07T11:30:00Z2017-02-07T12:00:00Z2017-02-07T12:30:00Z2017-02-07T13:00:00Z2017-02-07T13:30:00Z2017-02-07T14:00:00Z2017-02-07T14:30:00Z2017-02-07T15:00:00Z2017-02-07T15:30:00Z2017-02-07T16:00:00Z
"`; From d1d2c728feb1cc648d7bb41c3c8db804af7a4c58 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 14:04:59 +0200 Subject: [PATCH 18/24] [ML] handle empty string label, fix translation key --- .../ml/public/application/explorer/anomaly_timeline.tsx | 2 +- .../ml/public/application/explorer/swimlane_container.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index ab722bf0cd1d9..76f6785544132 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -161,7 +161,7 @@ export const AnomalyTimeline: FC = React.memo( diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index c67dc8650a071..c3c51a2ea4bfc 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -40,6 +40,7 @@ import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; +import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -273,6 +274,9 @@ export const SwimlaneContainer: FC = ({ // eui color subdued fill: `#6a717d`, padding: 8, + formatter: (laneLabel: string) => { + return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; + }, }, xAxisLabel: { visible: showTimeline, From 99b07178ace556c0eee45f909b17d12afbbf9e28 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 14:38:09 +0200 Subject: [PATCH 19/24] [ML] better positioning for the loading indicator --- .../explorer/swimlane_container.tsx | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index c3c51a2ea4bfc..92731e270673a 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -250,7 +250,7 @@ export const SwimlaneContainer: FC = ({ grid: { cellHeight: { min: CELL_HEIGHT, - max: CELL_HEIGHT, // 'fill', + max: CELL_HEIGHT, }, stroke: { width: 1, @@ -341,7 +341,10 @@ export const SwimlaneContainer: FC = ({ }} grow={false} > -
+
{showSwimlane && !isLoading && ( = ({ /> )} -
- {isLoading && ( - - + + + )} + {!isLoading && !showSwimlane && ( + {noDataWarning}} /> - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} - /> - )} + )} +
{isPaginationVisible && ( From 661aab6ccc11e3b3a57ba87a27adb077ec9c4083 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 14:49:30 +0200 Subject: [PATCH 20/24] [ML] update elastic/charts version --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2a53d30e19d19..f53edb7815106 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "@babel/register": "^7.10.5", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "23.1.0", + "@elastic/charts": "23.2.0", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8a63770898f0a..a9c817df9d107 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "23.1.0", + "@elastic/charts": "23.2.0", "@elastic/eui": "29.0.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/yarn.lock b/yarn.lock index abbb243046917..540fb47075ff6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,10 +1218,10 @@ dependencies: "@elastic/apm-rum-core" "^5.7.0" -"@elastic/charts@23.1.0": - version "23.1.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.1.0.tgz#343c3c54a308a0a54a6dd3387854ef143e8a7c7f" - integrity sha512-pBXCRSGd8Kzg0r60vjuSoB+ngbEnJQG+kFI7h6BbLFodp3/F1vONVsFvqG58ZBAsCb85/M3s4w0gR7qXNobr3g== +"@elastic/charts@23.2.0": + version "23.2.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.2.0.tgz#4266cf5d7864cac2bc274846ee96172d6fe47230" + integrity sha512-SMvHjuiSvKLuwnM+EkgAYuVp0dgJAgqKBFe0lQ5HquXDPjybesCbO+4bJnY2eOcvIyMdeqnr1y35oMfiNCGE4g== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 6e6321582c7ce3c830749aa66a81e1dc09a24355 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 17:05:25 +0200 Subject: [PATCH 21/24] [ML] fix getFormattedSeverityScore and showSwimlane condition --- .../plugins/ml/common/util/anomaly_utils.ts | 4 ++-- .../explorer/swimlane_container.tsx | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 633bc5bd47362..28b2f50ae2698 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -112,8 +112,8 @@ function getSeverityTypes() { /** * Return formatted severity score. */ -export function getFormattedSeverityScore(score: number) { - return score < 1 ? '< 1' : score.toFixed(2); +export function getFormattedSeverityScore(score: number): string { + return score < 1 ? '< 1' : String(parseInt(String(score), 10)); } // Returns a severity label (one of critical, major, minor, warning or unknown) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 92731e270673a..0a2791edb9c50 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -174,17 +174,6 @@ export const SwimlaneContainer: FC = ({ [chartWidth] ); - const showSwimlane = swimlaneData?.laneLabels?.length > 0 && swimlaneData?.points.length > 0; - - const isPaginationVisible = - (showSwimlane || isLoading) && - swimlaneLimit !== undefined && - onPaginationChange && - fromPage && - perPage; - - const rowsCount = swimlaneData?.laneLabels?.length ?? 0; - const swimLanePoints = useMemo(() => { const showFilterContext = filterActive === true && swimlaneType === SWIMLANE_TYPE.OVERALL; @@ -206,6 +195,17 @@ export const SwimlaneContainer: FC = ({ .filter((v) => v.value > 0); }, [swimlaneData?.points, filterActive, swimlaneType]); + const showSwimlane = swimlaneData?.laneLabels?.length > 0 && swimLanePoints.length > 0; + + const isPaginationVisible = + (showSwimlane || isLoading) && + swimlaneLimit !== undefined && + onPaginationChange && + fromPage && + perPage; + + const rowsCount = swimlaneData?.laneLabels?.length ?? 0; + const containerHeight = useMemo(() => { // Persists container height during loading to prevent page from jumping return isLoading From d30b08e6ae05be8b47e58bc95d484b4417fb1e02 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 17:13:36 +0200 Subject: [PATCH 22/24] [ML] fix selector for functional test --- x-pack/test/functional/services/ml/anomaly_explorer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts index 1a6d5cd09f2e2..fd430059f9f41 100644 --- a/x-pack/test/functional/services/ml/anomaly_explorer.ts +++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts @@ -88,7 +88,7 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid ); await testSubjects.clickWhenNotDisabled('mlAddAndEditDashboardButton'); const embeddable = await testSubjects.find('mlAnomalySwimlaneEmbeddableWrapper'); - const swimlane = await embeddable.findByClassName('ml-swimlanes'); + const swimlane = await embeddable.findByClassName('mlSwimLaneContainer'); expect(await swimlane.isDisplayed()).to.eql( true, 'Anomaly swimlane should be displayed in dashboard' From f3c7a9439a3aedb8ece8a5a5a5898ea7d6a2e447 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 17:20:06 +0200 Subject: [PATCH 23/24] [ML] change the legend alignment --- x-pack/plugins/ml/public/application/explorer/_explorer.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 1e2b0593081a9..d16a84a23c813 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -57,9 +57,10 @@ $borderRadius: $euiBorderRadius / 2; } .echLegendList { - margin-left: 170px !important; display: flex !important; justify-content: space-between !important; flex-wrap: nowrap; + position: absolute; + right: 0; } } From 7910908cd202afcb216473f592bbce5fb074a7df Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Oct 2020 19:13:57 +0200 Subject: [PATCH 24/24] [ML] update elastic charts --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a9721f05f997b..9f9ad9ead7096 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "@babel/register": "^7.10.5", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "23.2.0", + "@elastic/charts": "23.2.1", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 059c3bf744ae0..e5ebb874e58aa 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "23.2.0", + "@elastic/charts": "23.2.1", "@elastic/eui": "29.3.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/yarn.lock b/yarn.lock index 13e8fbf2b95ad..74e0bf8eb81e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,10 +1218,10 @@ dependencies: "@elastic/apm-rum-core" "^5.7.0" -"@elastic/charts@23.2.0": - version "23.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.2.0.tgz#4266cf5d7864cac2bc274846ee96172d6fe47230" - integrity sha512-SMvHjuiSvKLuwnM+EkgAYuVp0dgJAgqKBFe0lQ5HquXDPjybesCbO+4bJnY2eOcvIyMdeqnr1y35oMfiNCGE4g== +"@elastic/charts@23.2.1": + version "23.2.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.2.1.tgz#1f48629fe4597655a7f119fd019c4d5a2cbaf252" + integrity sha512-L2jUPAWwE0xLry6DcqcngVLCa9R32pfz5jW1fyOJRWSq1Fay2swOw4joBe8PmHpvl2s8EwWi9qWBORR1z3hUeQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0"