Skip to content

Commit

Permalink
Revert "[ML] Embeddable Anomaly Swimlane (#64056)"
Browse files Browse the repository at this point in the history
This reverts commit f62df99.
  • Loading branch information
spalger committed May 4, 2020
1 parent e3b9b94 commit dccb1dc
Show file tree
Hide file tree
Showing 61 changed files with 944 additions and 3,100 deletions.
4 changes: 1 addition & 3 deletions x-pack/plugins/ml/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
"home",
"licensing",
"usageCollection",
"share",
"embeddable",
"uiActions"
"share"
],
"optionalPlugins": [
"security",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import TooltipTrigger from 'react-popper-tooltip';
import React, { useRef, FC } from 'react';
import { TooltipValueFormatter } from '@elastic/charts';
import useObservable from 'react-use/lib/useObservable';

import './_index.scss';
import { chartTooltip$, ChartTooltipState, ChartTooltipValue } from './chart_tooltip_service';

import { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types';
import { ChartTooltipService, ChartTooltipValue, TooltipData } from './chart_tooltip_service';
type RefValue = HTMLElement | null;

function useRefWithCallback(chartTooltipState?: ChartTooltipState) {
const ref = useRef<RefValue>(null);

return (node: RefValue) => {
ref.current = node;

if (
node !== null &&
node.parentElement !== null &&
chartTooltipState !== undefined &&
chartTooltipState.isTooltipVisible
) {
const parentBounding = node.parentElement.getBoundingClientRect();

const { targetPosition, offset } = chartTooltipState;

const contentWidth = document.body.clientWidth - parentBounding.left;
const tooltipWidth = node.clientWidth;

let left = targetPosition.left + offset.x - parentBounding.left;
if (left + tooltipWidth > contentWidth) {
// the tooltip is hanging off the side of the page,
// so move it to the other side of the target
left = left - (tooltipWidth + offset.x);
}

const top = targetPosition.top + offset.y - parentBounding.top;

if (
chartTooltipState.tooltipPosition.left !== left ||
chartTooltipState.tooltipPosition.top !== top
) {
// render the tooltip with adjusted position.
chartTooltip$.next({
...chartTooltipState,
tooltipPosition: { left, top },
});
}
}
};
}

const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => {
if (!headerData) {
Expand All @@ -22,101 +63,48 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo
return formatter ? formatter(headerData) : headerData.label;
};

const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) => {
const [tooltipData, setData] = useState<TooltipData>([]);
const refCallback = useRef<ChildrenArg['triggerRef']>();
export const ChartTooltip: FC = () => {
const chartTooltipState = useObservable(chartTooltip$);
const chartTooltipElement = useRefWithCallback(chartTooltipState);

useEffect(() => {
const subscription = service.tooltipState$.subscribe(tooltipState => {
if (refCallback.current) {
// update trigger
refCallback.current(tooltipState.target);
}
setData(tooltipState.tooltipData);
});
return () => {
subscription.unsubscribe();
};
}, []);

const triggerCallback = useCallback(
(({ triggerRef }) => {
// obtain the reference to the trigger setter callback
// to update the target based on changes from the service.
refCallback.current = triggerRef;
// actual trigger is resolved by the service, hence don't render
return null;
}) as TooltipTriggerProps['children'],
[]
);

const tooltipCallback = useCallback(
(({ tooltipRef, getTooltipProps }) => {
return (
<div
{...getTooltipProps({
ref: tooltipRef,
className: 'mlChartTooltip',
})}
>
{tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && (
<div className="mlChartTooltip__header">{renderHeader(tooltipData[0])}</div>
)}
{tooltipData.length > 1 && (
<div className="mlChartTooltip__list">
{tooltipData
.slice(1)
.map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => {
const classes = classNames('mlChartTooltip__item', {
/* eslint @typescript-eslint/camelcase:0 */
echTooltip__rowHighlighted: isHighlighted,
});
return (
<div
key={`${seriesIdentifier.key}__${valueAccessor}`}
className={classes}
style={{
borderLeftColor: color,
}}
>
<span className="mlChartTooltip__label">{label}</span>
<span className="mlChartTooltip__value">{value}</span>
</div>
);
})}
</div>
)}
</div>
);
}) as TooltipTriggerProps['tooltip'],
[tooltipData]
);

const isTooltipShown = tooltipData.length > 0;

return (
<TooltipTrigger
placement="right-start"
trigger="none"
tooltipShown={isTooltipShown}
tooltip={tooltipCallback}
>
{triggerCallback}
</TooltipTrigger>
);
});

interface MlTooltipComponentProps {
children: (tooltipService: ChartTooltipService) => React.ReactElement;
}
if (chartTooltipState === undefined || !chartTooltipState.isTooltipVisible) {
return <div className="mlChartTooltip mlChartTooltip--hidden" ref={chartTooltipElement} />;
}

export const MlTooltipComponent: FC<MlTooltipComponentProps> = ({ children }) => {
const service = useMemo(() => new ChartTooltipService(), []);
const { tooltipData, tooltipHeaderFormatter, tooltipPosition } = chartTooltipState;
const transform = `translate(${tooltipPosition.left}px, ${tooltipPosition.top}px)`;

return (
<>
<Tooltip service={service} />
{children(service)}
</>
<div className="mlChartTooltip" style={{ transform }} ref={chartTooltipElement}>
{tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && (
<div className="mlChartTooltip__header">
{renderHeader(tooltipData[0], tooltipHeaderFormatter)}
</div>
)}
{tooltipData.length > 1 && (
<div className="mlChartTooltip__list">
{tooltipData
.slice(1)
.map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => {
const classes = classNames('mlChartTooltip__item', {
/* eslint @typescript-eslint/camelcase:0 */
echTooltip__rowHighlighted: isHighlighted,
});
return (
<div
key={`${seriesIdentifier.key}__${valueAccessor}`}
className={classes}
style={{
borderLeftColor: color,
}}
>
<span className="mlChartTooltip__label">{label}</span>
<span className="mlChartTooltip__value">{value}</span>
</div>
);
})}
</div>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { BehaviorSubject } from 'rxjs';

import { TooltipValue, TooltipValueFormatter } from '@elastic/charts';

export declare const getChartTooltipDefaultState: () => ChartTooltipState;

export interface ChartTooltipValue extends TooltipValue {
skipHeader?: boolean;
}

interface ChartTooltipState {
isTooltipVisible: boolean;
offset: ToolTipOffset;
targetPosition: ClientRect;
tooltipData: ChartTooltipValue[];
tooltipHeaderFormatter?: TooltipValueFormatter;
tooltipPosition: { left: number; top: number };
}

export declare const chartTooltip$: BehaviorSubject<ChartTooltipState>;

interface ToolTipOffset {
x: number;
y: number;
}

interface MlChartTooltipService {
show: (
tooltipData: ChartTooltipValue[],
target?: HTMLElement | null,
offset?: ToolTipOffset
) => void;
hide: () => void;
}

export declare const mlChartTooltipService: MlChartTooltipService;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { BehaviorSubject } from 'rxjs';

export const getChartTooltipDefaultState = () => ({
isTooltipVisible: false,
tooltipData: [],
offset: { x: 0, y: 0 },
targetPosition: { left: 0, top: 0 },
tooltipPosition: { left: 0, top: 0 },
});

export const chartTooltip$ = new BehaviorSubject(getChartTooltipDefaultState());

export const mlChartTooltipService = {
show: (tooltipData, target, offset = { x: 0, y: 0 }) => {
if (typeof target !== 'undefined' && target !== null) {
chartTooltip$.next({
...chartTooltip$.getValue(),
isTooltipVisible: true,
offset,
targetPosition: target.getBoundingClientRect(),
tooltipData,
});
}
},
hide: () => {
chartTooltip$.next({
...getChartTooltipDefaultState(),
isTooltipVisible: false,
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
ChartTooltipService,
getChartTooltipDefaultState,
TooltipData,
} from './chart_tooltip_service';
import { getChartTooltipDefaultState, mlChartTooltipService } from './chart_tooltip_service';

describe('ChartTooltipService', () => {
let service: ChartTooltipService;

beforeEach(() => {
service = new ChartTooltipService();
});

test('should update the tooltip state on show and hide', () => {
const spy = jest.fn();

service.tooltipState$.subscribe(spy);

expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState());

const update = [
{
label: 'new tooltip',
},
] as TooltipData;
const mockEl = document.createElement('div');

service.show(update, mockEl);

expect(spy).toHaveBeenCalledWith({
isTooltipVisible: true,
tooltipData: update,
offset: { x: 0, y: 0 },
target: mockEl,
});

service.hide();

expect(spy).toHaveBeenCalledWith({
isTooltipVisible: false,
tooltipData: ([] as unknown) as TooltipData,
offset: { x: 0, y: 0 },
target: null,
});
describe('ML - mlChartTooltipService', () => {
it('service API duck typing', () => {
expect(typeof mlChartTooltipService).toBe('object');
expect(typeof mlChartTooltipService.show).toBe('function');
expect(typeof mlChartTooltipService.hide).toBe('function');
});

test('update the tooltip state only on a new value', () => {
const spy = jest.fn();

service.tooltipState$.subscribe(spy);

expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState());

service.hide();

expect(spy).toHaveBeenCalledTimes(1);
it('should fail silently when target is not defined', () => {
expect(() => {
mlChartTooltipService.show(getChartTooltipDefaultState().tooltipData, null);
}).not.toThrow('Call to show() should fail silently.');
});
});
Loading

0 comments on commit dccb1dc

Please sign in to comment.