Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Embeddable Anomaly Swimlane #64056

Merged
merged 58 commits into from
May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ea0da2b
[ML] embeddables setup
darnautov Apr 16, 2020
9017723
[ML] fix initialization
darnautov Apr 16, 2020
088a888
[ML] ts refactoring
darnautov Apr 16, 2020
b32602e
[ML] refactor time_buckets.js
darnautov Apr 17, 2020
c735a25
[ML] async services
darnautov Apr 20, 2020
1d8c743
[ML] extract job_selector_flyout.tsx
darnautov Apr 20, 2020
c934869
[ML] fetch overall swimlane data
darnautov Apr 21, 2020
96463a6
[ML] import explorer styles
darnautov Apr 21, 2020
c808dda
[ML] revert package_globs.ts
darnautov Apr 21, 2020
84f7f9c
[ML] refactor with container, services with DI
darnautov Apr 22, 2020
f5e40fc
[ML] resize throttle, fetch based on chart width
darnautov Apr 22, 2020
2284c98
[ML] swimlane embeddable setup
darnautov Apr 22, 2020
46907aa
[ML] explorer service
darnautov Apr 23, 2020
2aefe0b
[ML] chart_tooltip_service
darnautov Apr 23, 2020
91cf91f
[ML] fix types
darnautov Apr 23, 2020
59f0a0e
[ML] overall type for single job with no influencers
darnautov Apr 23, 2020
49d0f79
[ML] improve anomaly_swimlane_initializer ux
darnautov Apr 23, 2020
8b8f713
[ML] fix services initialization, unsubscribe on destroy
darnautov Apr 23, 2020
4d6d713
[ML] support custom time range
darnautov Apr 23, 2020
9cb2bf0
[ML] add tooltip
darnautov Apr 23, 2020
326ff5c
[ML] rollback initGetSwimlaneBucketInterval
darnautov Apr 24, 2020
4818c7e
[ML] new tooltip service
darnautov Apr 24, 2020
b3a38d1
[ML] MlTooltipComponent with render props, fix warning
darnautov Apr 27, 2020
a94678d
Merge remote-tracking branch 'upstream/master' into ML-swimlane-embed…
darnautov Apr 27, 2020
7769bd0
[ML] fix typo in the filename
darnautov Apr 27, 2020
7e3eec7
[ML] remove redundant time range output
darnautov Apr 27, 2020
6b1d182
[ML] fix time_buckets.test.js jest tests
darnautov Apr 27, 2020
df0b3d4
[ML] fix explorer chart tests
darnautov Apr 27, 2020
63b42ee
[ML] swimlane tests
darnautov Apr 27, 2020
adafba8
[ML] store job ids instead of complete job objects
darnautov Apr 27, 2020
c753a7e
[ML] swimlane limit input
darnautov Apr 28, 2020
2a3c5f7
[ML] memo tooltip component, loading indicator
darnautov Apr 28, 2020
936015d
Merge remote-tracking branch 'upstream/master' into ML-swimlane-embed…
darnautov Apr 28, 2020
65f64d3
[ML] scrollable content
darnautov Apr 28, 2020
3fd8c45
[ML] support query and filters
darnautov Apr 28, 2020
d0df7ef
[ML] handle query syntax errors
darnautov Apr 28, 2020
cf990ab
[ML] rename anomaly_swimlane_service
darnautov Apr 28, 2020
5801a72
[ML] introduce constants
darnautov Apr 28, 2020
0fd4942
[ML] edit panel title during setup
darnautov Apr 28, 2020
ad9f9f0
[ML] withTimeRangeSelector
darnautov Apr 28, 2020
9fac42a
[ML] rename explorer_service
darnautov Apr 28, 2020
17515b1
[ML] getJobs$ method with one API call
darnautov Apr 29, 2020
4ac71c1
Merge remote-tracking branch 'upstream/master' into ML-swimlane-embed…
darnautov Apr 29, 2020
59a25a6
[ML] fix groups selection
darnautov Apr 29, 2020
4894db5
[ML] swimlane input resolver hook
darnautov Apr 29, 2020
8c7691a
[ML] useSwimlaneInputResolver tests
darnautov Apr 29, 2020
37774dc
[ML] factory test
darnautov Apr 30, 2020
00cd6d1
[ML] container test
darnautov Apr 30, 2020
172807e
[ML] set wrapper
darnautov Apr 30, 2020
ce66cd2
[ML] tooltip tests
darnautov Apr 30, 2020
b1bdde5
[ML] fix displayScore
darnautov Apr 30, 2020
57c3532
[ML] label colors
darnautov Apr 30, 2020
87182b6
Merge branch 'master' into ML-swimlane-embeddable
elasticmachine May 4, 2020
99c273f
Merge branch 'master' into ML-swimlane-embeddable
elasticmachine May 4, 2020
280c324
Merge branch 'master' into ML-swimlane-embeddable
elasticmachine May 4, 2020
cef8862
[ML] support edit mode
darnautov May 4, 2020
ac463aa
Merge remote-tracking branch 'origin/ML-swimlane-embeddable' into ML-…
darnautov May 4, 2020
980e6cf
[ML] call super render
darnautov May 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion x-pack/plugins/ml/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"home",
"licensing",
"usageCollection",
"share"
"share",
"embeddable",
"uiActions"
],
"optionalPlugins": [
"security",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,15 @@
* 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 React, { useRef, FC } from 'react';
import TooltipTrigger from 'react-popper-tooltip';
import { TooltipValueFormatter } from '@elastic/charts';
import useObservable from 'react-use/lib/useObservable';

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

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 },
});
}
}
};
}
import { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types';
import { ChartTooltipService, ChartTooltipValue, TooltipData } from './chart_tooltip_service';

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

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

if (chartTooltipState === undefined || !chartTooltipState.isTooltipVisible) {
return <div className="mlChartTooltip mlChartTooltip--hidden" ref={chartTooltipElement} />;
}
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 { tooltipData, tooltipHeaderFormatter, tooltipPosition } = chartTooltipState;
const transform = `translate(${tooltipPosition.left}px, ${tooltipPosition.top}px)`;
const isTooltipShown = tooltipData.length > 0;

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

interface MlTooltipComponentProps {
children: (tooltipService: ChartTooltipService) => React.ReactElement;
}

export const MlTooltipComponent: FC<MlTooltipComponentProps> = ({ children }) => {
const service = useMemo(() => new ChartTooltipService(), []);

return (
<>
<Tooltip service={service} />
{children(service)}
</>
);
};

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,61 @@
* you may not use this file except in compliance with the Elastic License.
*/

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

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');
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,
});
});

it('should fail silently when target is not defined', () => {
expect(() => {
mlChartTooltipService.show(getChartTooltipDefaultState().tooltipData, null);
}).not.toThrow('Call to show() should fail silently.');
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);
});
});
Loading