mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Attach the anomaly charts embeddable to Case (#139628)
* reusable get_embeddable_component.tsx * register attachment type * useCasesModal * fix anomaly charts input resolver and fetching * fix jest test * functional tests * add anomaly charts to ml functional test services * check for undefined cases modal * remove todo * filter and query condition * update assertion * stabilize set debug state * memoize component * move embeddable components init * refactor react lazy * refactor react lazy for swim lane * memo callbacks * update deps for attachment
This commit is contained in:
parent
2a3cf660ab
commit
cb7b03106f
24 changed files with 560 additions and 188 deletions
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import { CommentType } from '@kbn/cases-plugin/common';
|
||||
import { useMlKibana } from './kibana_context';
|
||||
import type { MappedEmbeddableTypeOf, MlEmbeddableTypes } from '../../../embeddables';
|
||||
|
||||
/**
|
||||
* Returns a callback for opening the cases modal with provided attachment state.
|
||||
*/
|
||||
export const useCasesModal = <EmbeddableType extends MlEmbeddableTypes>(
|
||||
embeddableType: EmbeddableType
|
||||
) => {
|
||||
const {
|
||||
services: { cases },
|
||||
} = useMlKibana();
|
||||
|
||||
const selectCaseModal = cases?.hooks.getUseCasesAddToExistingCaseModal();
|
||||
|
||||
return useCallback(
|
||||
(persistableState: Partial<Omit<MappedEmbeddableTypeOf<EmbeddableType>, 'id'>>) => {
|
||||
const persistableStateAttachmentState = {
|
||||
...persistableState,
|
||||
// Creates unique id based on the input
|
||||
id: stringHash(JSON.stringify(persistableState)).toString(),
|
||||
};
|
||||
|
||||
if (!selectCaseModal) {
|
||||
throw new Error('Cases modal is not available');
|
||||
}
|
||||
|
||||
selectCaseModal.open({
|
||||
attachments: [
|
||||
{
|
||||
type: CommentType.persistableState,
|
||||
persistableStateAttachmentTypeId: embeddableType,
|
||||
// TODO Cases: improve type for persistableStateAttachmentState with io-ts
|
||||
persistableStateAttachmentState: JSON.parse(
|
||||
JSON.stringify(persistableStateAttachmentState)
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
[embeddableType]
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, FC } from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
|
@ -15,6 +15,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service';
|
||||
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
|
||||
import { TimeRangeBounds } from '../util/time_buckets';
|
||||
|
@ -37,13 +41,25 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
const {
|
||||
services: {
|
||||
application: { capabilities },
|
||||
cases,
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const globalTimeRange = useTimeRangeUpdates(true);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false);
|
||||
|
||||
const closePopoverOnAction = useCallback(
|
||||
(actionCallback: Function) => {
|
||||
setIsMenuOpen(false);
|
||||
actionCallback();
|
||||
},
|
||||
[setIsMenuOpen]
|
||||
);
|
||||
|
||||
const openCasesModal = useCasesModal(ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE);
|
||||
|
||||
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
|
||||
const casesPrivileges = cases?.helpers.canUseCases();
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const items = [];
|
||||
|
@ -51,7 +67,7 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key="addToDashboard"
|
||||
onClick={setIsAddDashboardActive.bind(null, true)}
|
||||
onClick={closePopoverOnAction.bind(null, setIsAddDashboardActive.bind(null, true))}
|
||||
data-test-subj="mlAnomalyAddChartsToDashboardButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -61,15 +77,37 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (!!casesPrivileges?.create || !!casesPrivileges?.update) {
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key="attachToCase"
|
||||
onClick={closePopoverOnAction.bind(
|
||||
null,
|
||||
openCasesModal.bind(null, {
|
||||
jobIds: selectedJobs?.map((v) => v.id),
|
||||
timeRange: globalTimeRange,
|
||||
maxSeriesToPlot: DEFAULT_MAX_SERIES_TO_PLOT,
|
||||
})
|
||||
)}
|
||||
data-test-subj="mlAnomalyAttachChartsToCasesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.attachToCaseLabel"
|
||||
defaultMessage="Attach to case"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}, [canEditDashboards]);
|
||||
}, [canEditDashboards, globalTimeRange, closePopoverOnAction, selectedJobs]);
|
||||
|
||||
const jobIds = selectedJobs.map(({ id }) => id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuItems.length > 0 && chartsCount > 0 && (
|
||||
<EuiFlexItem grow={false} style={{ marginLeft: 'auto', alignSelf: 'baseline' }}>
|
||||
{menuItems.length > 0 && chartsCount > 0 ? (
|
||||
<EuiFlexItem grow={false} css={{ marginLeft: 'auto', alignSelf: 'baseline' }}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
|
@ -92,7 +130,7 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
<EuiContextMenuPanel items={menuItems} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
) : null}
|
||||
{isAddDashboardsActive && selectedJobs ? (
|
||||
<AddAnomalyChartsToDashboardControl
|
||||
onClose={async () => {
|
||||
|
|
|
@ -26,10 +26,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { CommentType } from '@kbn/cases-plugin/common';
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import type { AnomalySwimlaneEmbeddableInput } from '../../embeddables';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..';
|
||||
import {
|
||||
OVERALL_LABEL,
|
||||
|
@ -164,34 +162,18 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
[severityUpdate, swimLaneSeverity]
|
||||
);
|
||||
|
||||
const openCasesModalCallback = useCasesModal(ANOMALY_SWIMLANE_EMBEDDABLE_TYPE);
|
||||
|
||||
const openCasesModal = useCallback(
|
||||
(swimLaneType: SwimlaneType) => {
|
||||
const persistableStateAttachmentState = {
|
||||
openCasesModalCallback({
|
||||
swimlaneType: swimLaneType,
|
||||
...(swimLaneType === SWIMLANE_TYPE.VIEW_BY ? { viewBy: viewBySwimlaneFieldName } : {}),
|
||||
jobIds: selectedJobs?.map((v) => v.id),
|
||||
timeRange: globalTimeRange,
|
||||
} as AnomalySwimlaneEmbeddableInput;
|
||||
|
||||
// Creates unique id based on the input
|
||||
persistableStateAttachmentState.id = stringHash(
|
||||
JSON.stringify(persistableStateAttachmentState)
|
||||
).toString();
|
||||
|
||||
selectCaseModal!.open({
|
||||
attachments: [
|
||||
{
|
||||
type: CommentType.persistableState,
|
||||
persistableStateAttachmentTypeId: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
|
||||
// TODO Cases: improve type for persistableStateAttachmentState with io-ts
|
||||
persistableStateAttachmentState: JSON.parse(
|
||||
JSON.stringify(persistableStateAttachmentState)
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
[selectCaseModal, selectedJobs, globalTimeRange, viewBySwimlaneFieldName]
|
||||
[openCasesModalCallback, selectedJobs, globalTimeRange, viewBySwimlaneFieldName]
|
||||
);
|
||||
|
||||
const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]);
|
||||
|
|
|
@ -49,7 +49,7 @@ describe('AnomalyExplorerChartsService', () => {
|
|||
);
|
||||
|
||||
timefilterMock = createTimefilterMock();
|
||||
timefilterMock.getActiveBounds.mockReturnValue({
|
||||
timefilterMock.getBounds.mockReturnValue({
|
||||
min: moment(1486656000000),
|
||||
max: moment(1486670399999),
|
||||
});
|
||||
|
|
|
@ -110,7 +110,7 @@ export class AnomalyExplorerChartsService {
|
|||
severity = 0,
|
||||
maxSeries?: number
|
||||
): Observable<ExplorerChartsData> {
|
||||
const bounds = this.timeFilter.getActiveBounds();
|
||||
const bounds = this.getTimeBounds();
|
||||
const boundsMin = bounds?.min ? bounds.min.valueOf() : undefined;
|
||||
const boundsMax = bounds?.max ? bounds.max.valueOf() : undefined;
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { memoize } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { AnomalyChartsEmbeddableInput } from '../embeddables';
|
||||
|
||||
export const initComponent = memoize(
|
||||
(fieldFormats: FieldFormatsStart, EmbeddableComponent: FC<AnomalyChartsEmbeddableInput>) => {
|
||||
return React.memo(
|
||||
(props: PersistableStateAttachmentViewProps) => {
|
||||
const { persistableStateAttachmentState } = props;
|
||||
|
||||
const dataFormatter = fieldFormats.deserialize({
|
||||
id: FIELD_FORMAT_IDS.DATE,
|
||||
});
|
||||
|
||||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as AnomalyChartsEmbeddableInput;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type={'inline'}
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EmbeddableComponent {...inputProps} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
deepEqual(
|
||||
prevProps.persistableStateAttachmentState,
|
||||
nextProps.persistableStateAttachmentState
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { memoize } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { AnomalySwimlaneEmbeddableInput } from '..';
|
||||
|
||||
export const initComponent = memoize(
|
||||
(fieldFormats: FieldFormatsStart, EmbeddableComponent: FC<AnomalySwimlaneEmbeddableInput>) => {
|
||||
return React.memo(
|
||||
(props: PersistableStateAttachmentViewProps) => {
|
||||
const { persistableStateAttachmentState } = props;
|
||||
|
||||
const dataFormatter = fieldFormats.deserialize({
|
||||
id: FIELD_FORMAT_IDS.DATE,
|
||||
});
|
||||
|
||||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as AnomalySwimlaneEmbeddableInput;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type={'inline'}
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
...(inputProps.viewBy
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.viewByLabel"
|
||||
defaultMessage="View by"
|
||||
/>
|
||||
),
|
||||
description: inputProps.viewBy,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EmbeddableComponent {...inputProps} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
deepEqual(
|
||||
prevProps.persistableStateAttachmentState,
|
||||
nextProps.persistableStateAttachmentState
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, getEmbeddableComponent } from '../embeddables';
|
||||
import type { MlStartDependencies } from '../plugin';
|
||||
import { PLUGIN_ICON } from '../../common/constants/app';
|
||||
|
||||
export function registerAnomalyChartsCasesAttachment(
|
||||
cases: CasesUiSetup,
|
||||
coreStart: CoreStart,
|
||||
pluginStart: MlStartDependencies
|
||||
) {
|
||||
const EmbeddableComponent = getEmbeddableComponent(
|
||||
ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
|
||||
coreStart,
|
||||
pluginStart
|
||||
);
|
||||
|
||||
cases.attachmentFramework.registerPersistableState({
|
||||
id: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
|
||||
icon: PLUGIN_ICON,
|
||||
displayName: i18n.translate('xpack.ml.cases.anomalyCharts.displayName', {
|
||||
defaultMessage: 'Anomaly charts',
|
||||
}),
|
||||
getAttachmentViewObject: () => ({
|
||||
event: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.embeddableAddedEvent"
|
||||
defaultMessage="added the Anomaly Charts embeddable"
|
||||
/>
|
||||
),
|
||||
timelineAvatar: PLUGIN_ICON,
|
||||
children: React.lazy(async () => {
|
||||
const { initComponent } = await import('./anomaly_charts_attachments');
|
||||
return {
|
||||
default: initComponent(pluginStart.fieldFormats, EmbeddableComponent),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -8,36 +8,27 @@
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonIcon, EuiDescriptionList } from '@elastic/eui';
|
||||
import { CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
|
||||
import { MlStartDependencies } from '../plugin';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, AnomalySwimlaneEmbeddableInput } from '..';
|
||||
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { getEmbeddableComponent } from '../embeddables';
|
||||
import type { MlStartDependencies } from '../plugin';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..';
|
||||
import { PLUGIN_ICON } from '../../common/constants/app';
|
||||
import { getAnomalySwimLaneEmbeddableComponent } from '../embeddables/anomaly_swimlane';
|
||||
|
||||
const AttachmentActions: React.FC = () => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="test-attachment-action"
|
||||
onClick={() => {}}
|
||||
iconType="boxesHorizontal"
|
||||
aria-label="See attachment"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function registerAnomalySwimLaneCasesAttachment(
|
||||
cases: CasesUiSetup,
|
||||
coreStart: CoreStart,
|
||||
pluginStart: MlStartDependencies
|
||||
) {
|
||||
const EmbeddableComponent = getEmbeddableComponent(
|
||||
ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
|
||||
coreStart,
|
||||
pluginStart
|
||||
);
|
||||
|
||||
cases.attachmentFramework.registerPersistableState({
|
||||
id: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
|
||||
icon: PLUGIN_ICON,
|
||||
// TODO check where this name is presented
|
||||
displayName: i18n.translate('xpack.ml.cases.anomalySwimLane.displayName', {
|
||||
defaultMessage: 'Anomaly swim lane',
|
||||
}),
|
||||
|
@ -49,69 +40,11 @@ export function registerAnomalySwimLaneCasesAttachment(
|
|||
/>
|
||||
),
|
||||
timelineAvatar: PLUGIN_ICON,
|
||||
actions: <AttachmentActions />,
|
||||
children: React.lazy(() => {
|
||||
return Promise.resolve().then(() => {
|
||||
const EmbeddableComponent = getAnomalySwimLaneEmbeddableComponent(coreStart, pluginStart);
|
||||
|
||||
return {
|
||||
default: React.memo((props: PersistableStateAttachmentViewProps) => {
|
||||
const { persistableStateAttachmentState } = props;
|
||||
|
||||
const dataFormatter = pluginStart.fieldFormats.deserialize({
|
||||
id: FIELD_FORMAT_IDS.DATE,
|
||||
});
|
||||
|
||||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as AnomalySwimlaneEmbeddableInput;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type={'inline'}
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
...(inputProps.viewBy
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.viewByLabel"
|
||||
defaultMessage="View by"
|
||||
/>
|
||||
),
|
||||
description: inputProps.viewBy,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EmbeddableComponent {...inputProps} />
|
||||
</>
|
||||
);
|
||||
}),
|
||||
};
|
||||
});
|
||||
children: React.lazy(async () => {
|
||||
const { initComponent } = await import('./anomaly_swim_lane_attachment');
|
||||
return {
|
||||
default: initComponent(pluginStart.fieldFormats, EmbeddableComponent),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { registerAnomalyChartsCasesAttachment } from './register_anomaly_charts_attachment';
|
||||
import { MlStartDependencies } from '../plugin';
|
||||
import { registerAnomalySwimLaneCasesAttachment } from './register_anomaly_swim_lane_attachment';
|
||||
|
||||
|
@ -16,4 +17,5 @@ export function registerCasesAttachments(
|
|||
pluginStart: MlStartDependencies
|
||||
) {
|
||||
registerAnomalySwimLaneCasesAttachment(cases, coreStart, pluginStart);
|
||||
registerAnomalyChartsCasesAttachment(cases, coreStart, pluginStart);
|
||||
}
|
||||
|
|
|
@ -81,9 +81,11 @@ export function useAnomalyChartsInputResolver(
|
|||
|
||||
anomalyExplorerService.setTimeRange(timeRangeInput);
|
||||
|
||||
let influencersFilterQuery: InfluencersFilterQuery;
|
||||
let influencersFilterQuery: InfluencersFilterQuery | undefined;
|
||||
try {
|
||||
influencersFilterQuery = processFilters(filters, query);
|
||||
if (filters || query) {
|
||||
influencersFilterQuery = processFilters(filters, query);
|
||||
}
|
||||
} catch (e) {
|
||||
// handle query syntax errors
|
||||
setError(e);
|
||||
|
|
|
@ -1,38 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
EmbeddableRoot,
|
||||
useEmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, AnomalySwimlaneEmbeddableInput } from '../..';
|
||||
import type { MlStartDependencies } from '../../plugin';
|
||||
|
||||
export function getEmbeddableComponent(core: CoreStart, plugins: MlStartDependencies) {
|
||||
const { embeddable: embeddableStart } = plugins;
|
||||
const factory = embeddableStart.getEmbeddableFactory(ANOMALY_SWIMLANE_EMBEDDABLE_TYPE)!;
|
||||
return (props: AnomalySwimlaneEmbeddableInput) => {
|
||||
return <EmbeddableRootWrapper factory={factory} input={props} />;
|
||||
};
|
||||
}
|
||||
|
||||
const EmbeddableRootWrapper: FC<{
|
||||
factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>;
|
||||
input: AnomalySwimlaneEmbeddableInput;
|
||||
}> = ({ factory, input }) => {
|
||||
const [embeddable, loading, error] = useEmbeddableFactory({ factory, input });
|
||||
if (loading) {
|
||||
return <EuiLoadingChart />;
|
||||
}
|
||||
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
|
||||
};
|
|
@ -6,4 +6,3 @@
|
|||
*/
|
||||
|
||||
export { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane_embeddable_factory';
|
||||
export { getEmbeddableComponent as getAnomalySwimLaneEmbeddableComponent } from './embeddable_component';
|
||||
|
|
|
@ -151,7 +151,7 @@ export function useSwimlaneInputResolver(
|
|||
|
||||
let appliedFilters: any;
|
||||
try {
|
||||
if (filters && query) {
|
||||
if (filters || query) {
|
||||
appliedFilters = processFilters(filters, query, CONTROLLED_BY_SWIM_LANE_FILTER);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -7,3 +7,8 @@
|
|||
|
||||
export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane' as const;
|
||||
export const ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE = 'ml_anomaly_charts' as const;
|
||||
|
||||
export type AnomalySwimLaneEmbeddableType = typeof ANOMALY_SWIMLANE_EMBEDDABLE_TYPE;
|
||||
export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE;
|
||||
|
||||
export type MlEmbeddableTypes = AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableRoot,
|
||||
useEmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import type { MappedEmbeddableTypeOf } from './types';
|
||||
import type { MlStartDependencies } from '../plugin';
|
||||
import type { MlEmbeddableTypes } from './constants';
|
||||
|
||||
/**
|
||||
* Gets an instance of an embeddable component of requested type.
|
||||
* @param embeddableType
|
||||
* @param core
|
||||
* @param plugins
|
||||
*/
|
||||
export function getEmbeddableComponent<EmbeddableType extends MlEmbeddableTypes>(
|
||||
embeddableType: EmbeddableType,
|
||||
core: CoreStart,
|
||||
plugins: MlStartDependencies
|
||||
) {
|
||||
const { embeddable: embeddableStart } = plugins;
|
||||
const factory =
|
||||
embeddableStart.getEmbeddableFactory<MappedEmbeddableTypeOf<EmbeddableType>>(embeddableType);
|
||||
|
||||
if (!factory) {
|
||||
throw new Error(`Embeddable type "${embeddableType}" has not been registered.`);
|
||||
}
|
||||
|
||||
return React.memo((props: MappedEmbeddableTypeOf<EmbeddableType>) => {
|
||||
return <EmbeddableRootWrapper factory={factory} input={props} />;
|
||||
});
|
||||
}
|
||||
|
||||
interface EmbeddableRootWrapperProps<TMlEmbeddableInput extends EmbeddableInput> {
|
||||
factory: EmbeddableFactory<TMlEmbeddableInput>;
|
||||
input: TMlEmbeddableInput;
|
||||
}
|
||||
|
||||
const EmbeddableRootWrapper = <TMlEmbeddableInput extends EmbeddableInput>({
|
||||
factory,
|
||||
input,
|
||||
}: EmbeddableRootWrapperProps<TMlEmbeddableInput>) => {
|
||||
const [embeddable, loading, error] = useEmbeddableFactory({ factory, input });
|
||||
if (loading) {
|
||||
return <EuiLoadingChart />;
|
||||
}
|
||||
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
|
||||
};
|
|
@ -13,6 +13,8 @@ import { AnomalyChartsEmbeddableFactory } from './anomaly_charts';
|
|||
export * from './constants';
|
||||
export * from './types';
|
||||
|
||||
export { getEmbeddableComponent } from './get_embeddable_component';
|
||||
|
||||
export function registerEmbeddables(embeddable: EmbeddableSetup, core: MlCoreSetup) {
|
||||
const anomalySwimlaneEmbeddableFactory = new AnomalySwimlaneEmbeddableFactory(
|
||||
core.getStartServices
|
||||
|
|
|
@ -22,6 +22,9 @@ import { EntityField } from '../../common/util/anomaly_utils';
|
|||
import {
|
||||
ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
|
||||
ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
|
||||
AnomalyExplorerChartsEmbeddableType,
|
||||
AnomalySwimLaneEmbeddableType,
|
||||
MlEmbeddableTypes,
|
||||
} from './constants';
|
||||
import { MlResultsService } from '../application/services/results_service';
|
||||
|
||||
|
@ -127,3 +130,10 @@ export function isAnomalyExplorerEmbeddable(
|
|||
arg.embeddable.type === ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
export type MappedEmbeddableTypeOf<TEmbeddableType extends MlEmbeddableTypes> =
|
||||
TEmbeddableType extends AnomalySwimLaneEmbeddableType
|
||||
? AnomalySwimlaneEmbeddableInput
|
||||
: TEmbeddableType extends AnomalyExplorerChartsEmbeddableType
|
||||
? AnomalyChartsEmbeddableInput
|
||||
: unknown;
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs';
|
||||
import { AnomalySwimlaneEmbeddableInput } from '@kbn/ml-plugin/public';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import type { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs';
|
||||
import type { AnomalySwimlaneEmbeddableInput } from '@kbn/ml-plugin/public';
|
||||
import type { AnomalyChartsEmbeddableInput } from '@kbn/ml-plugin/public/embeddables';
|
||||
import type { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { USER } from '../../../services/ml/security_common';
|
||||
|
||||
// @ts-expect-error not full interface
|
||||
|
@ -209,7 +210,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']);
|
||||
|
||||
await ml.testExecution.logTestStep('renders anomaly explorer charts');
|
||||
// TODO check why count changed from 4 to 5
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5);
|
||||
|
||||
await ml.testExecution.logTestStep('updates top influencers list');
|
||||
|
@ -410,7 +410,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await ml.anomalyExplorer.attachSwimLaneToCase('viewBy', {
|
||||
title: 'ML Test case',
|
||||
description: 'Case with an anomaly swim lane',
|
||||
tag: 'ml_case',
|
||||
tag: 'ml_swim_lane_case',
|
||||
});
|
||||
|
||||
const expectedAttachment = {
|
||||
|
@ -429,7 +429,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
{
|
||||
title: 'ML Test case',
|
||||
description: 'Case with an anomaly swim lane',
|
||||
tag: 'ml_case',
|
||||
tag: 'ml_swim_lane_case',
|
||||
reporter: USER.ML_POWERUSER,
|
||||
},
|
||||
expectedAttachment,
|
||||
|
@ -447,6 +447,60 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await ml.anomalyExplorer.addAndEditSwimlaneInDashboard('ML Test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Anomaly Charts as embeddable', function () {
|
||||
beforeEach(async () => {
|
||||
await ml.navigation.navigateToAnomalyExplorer(
|
||||
testData.jobConfig.job_id,
|
||||
{
|
||||
from: '2016-02-07T00%3A00%3A00.000Z',
|
||||
to: '2016-02-11T23%3A59%3A54.000Z',
|
||||
},
|
||||
() => elasticChart.setNewChartUiDebugFlag(true)
|
||||
);
|
||||
|
||||
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
|
||||
await ml.commonUI.waitForDatePickerIndicatorLoaded();
|
||||
|
||||
await ml.testExecution.logTestStep('clicks on the Overall swim lane cell');
|
||||
const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0];
|
||||
await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, {
|
||||
x: sampleCell.x + cellSize,
|
||||
y: sampleCell.y + cellSize,
|
||||
});
|
||||
await ml.swimLane.waitForSwimLanesToLoad();
|
||||
});
|
||||
|
||||
it('attaches an embeddable to a case', async () => {
|
||||
await ml.anomalyExplorer.attachAnomalyChartsToCase({
|
||||
title: 'ML Charts Test case',
|
||||
description: 'Case with an anomaly charts attachment',
|
||||
tag: 'ml_anomaly_charts',
|
||||
});
|
||||
|
||||
const expectedAttachment = {
|
||||
jobIds: [testData.jobConfig.job_id],
|
||||
timeRange: {
|
||||
from: '2016-02-07T00:00:00.000Z',
|
||||
to: '2016-02-11T23:59:54.000Z',
|
||||
},
|
||||
maxSeriesToPlot: 6,
|
||||
} as AnomalyChartsEmbeddableInput;
|
||||
|
||||
expectedAttachment.id = stringHash(JSON.stringify(expectedAttachment)).toString();
|
||||
|
||||
await ml.cases.assertCaseWithAnomalyChartsAttachment(
|
||||
{
|
||||
title: 'ML Charts Test case',
|
||||
description: 'Case with an anomaly charts attachment',
|
||||
tag: 'ml_anomaly_charts',
|
||||
reporter: USER.ML_POWERUSER,
|
||||
},
|
||||
expectedAttachment,
|
||||
6
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
32
x-pack/test/functional/services/ml/anomaly_charts.ts
Normal file
32
x-pack/test/functional/services/ml/anomaly_charts.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ProvidedType } from '@kbn/test';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export type MlAnomalyCharts = ProvidedType<typeof AnomalyChartsProvider>;
|
||||
|
||||
export function AnomalyChartsProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
return {
|
||||
async assertAnomalyExplorerChartsCount(
|
||||
chartsContainerSubj: string,
|
||||
expectedChartsCount: number
|
||||
) {
|
||||
const chartsContainer = await testSubjects.find(chartsContainerSubj);
|
||||
const actualChartsCount = (
|
||||
await chartsContainer.findAllByClassName('ml-explorer-chart-container', 3000)
|
||||
).length;
|
||||
expect(actualChartsCount).to.eql(
|
||||
expectedChartsCount,
|
||||
`Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -10,11 +10,12 @@ import expect from '@kbn/expect';
|
|||
import type { SwimlaneType } from '@kbn/ml-plugin/public/application/explorer/explorer_constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import type { CreateCaseParams } from '../cases/create';
|
||||
import { MlAnomalyCharts } from './anomaly_charts';
|
||||
|
||||
export function MachineLearningAnomalyExplorerProvider({
|
||||
getPageObject,
|
||||
getService,
|
||||
}: FtrProviderContext) {
|
||||
export function MachineLearningAnomalyExplorerProvider(
|
||||
{ getPageObject, getService }: FtrProviderContext,
|
||||
anomalyCharts: MlAnomalyCharts
|
||||
) {
|
||||
const dashboardPage = getPageObject('dashboard');
|
||||
const retry = getService('retry');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
@ -174,13 +175,9 @@ export function MachineLearningAnomalyExplorerProvider({
|
|||
},
|
||||
|
||||
async assertAnomalyExplorerChartsCount(expectedChartsCount: number) {
|
||||
const chartsContainer = await testSubjects.find('mlExplorerChartsContainer');
|
||||
const actualChartsCount = (
|
||||
await chartsContainer.findAllByClassName('ml-explorer-chart-container', 3000)
|
||||
).length;
|
||||
expect(actualChartsCount).to.eql(
|
||||
expectedChartsCount,
|
||||
`Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}`
|
||||
await anomalyCharts.assertAnomalyExplorerChartsCount(
|
||||
'mlExplorerChartsContainer',
|
||||
expectedChartsCount
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -203,5 +200,12 @@ export function MachineLearningAnomalyExplorerProvider({
|
|||
}' (got '${isEnabled ? 'enabled' : 'disabled'}')`
|
||||
);
|
||||
},
|
||||
|
||||
async attachAnomalyChartsToCase(params: CreateCaseParams) {
|
||||
await testSubjects.click('mlExplorerAnomalyPanelMenu');
|
||||
await testSubjects.click('mlAnomalyAttachChartsToCasesButton');
|
||||
|
||||
await cases.create.createCaseFromModal(params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SwimlaneType } from '@kbn/ml-plugin/public/application/explorer/explorer_constants';
|
||||
import { AnomalySwimlaneEmbeddableInput } from '@kbn/ml-plugin/public';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { MlAnomalySwimLane } from './swim_lane';
|
||||
import type { SwimlaneType } from '@kbn/ml-plugin/public/application/explorer/explorer_constants';
|
||||
import type { AnomalySwimlaneEmbeddableInput } from '@kbn/ml-plugin/public';
|
||||
import type { AnomalyChartsEmbeddableInput } from '@kbn/ml-plugin/public/embeddables';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import type { MlAnomalySwimLane } from './swim_lane';
|
||||
import type { MlAnomalyCharts } from './anomaly_charts';
|
||||
|
||||
export interface CaseParams {
|
||||
title: string;
|
||||
|
@ -23,7 +25,8 @@ export interface Attachment {
|
|||
|
||||
export function MachineLearningCasesProvider(
|
||||
{ getPageObject, getService }: FtrProviderContext,
|
||||
mlAnomalySwimLane: MlAnomalySwimLane
|
||||
mlAnomalySwimLane: MlAnomalySwimLane,
|
||||
mlAnomalyCharts: MlAnomalyCharts
|
||||
) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const cases = getService('cases');
|
||||
|
@ -38,6 +41,15 @@ export function MachineLearningCasesProvider(
|
|||
await cases.casesTable.goToFirstListedCase();
|
||||
},
|
||||
|
||||
async assertBasicCaseProps(params: CaseParams) {
|
||||
await this.openCaseInCasesApp(params.tag);
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
|
||||
// Assert case details
|
||||
await cases.singleCase.assertCaseTitle(params.title);
|
||||
await cases.singleCase.assertCaseDescription(params.description);
|
||||
},
|
||||
|
||||
async assertCaseWithAnomalySwimLaneAttachment(
|
||||
params: CaseParams,
|
||||
attachment: AnomalySwimlaneEmbeddableInput,
|
||||
|
@ -45,12 +57,7 @@ export function MachineLearningCasesProvider(
|
|||
yAxisLabelCount: number;
|
||||
}
|
||||
) {
|
||||
await this.openCaseInCasesApp(params.tag);
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
|
||||
// Assert case details
|
||||
await cases.singleCase.assertCaseTitle(params.title);
|
||||
await cases.singleCase.assertCaseDescription(params.description);
|
||||
await this.assertBasicCaseProps(params);
|
||||
|
||||
await testSubjects.existOrFail('comment-persistableState-ml_anomaly_swimlane');
|
||||
|
||||
|
@ -61,5 +68,19 @@ export function MachineLearningCasesProvider(
|
|||
expectedSwimLaneState.yAxisLabelCount
|
||||
);
|
||||
},
|
||||
|
||||
async assertCaseWithAnomalyChartsAttachment(
|
||||
params: CaseParams,
|
||||
attachment: AnomalyChartsEmbeddableInput,
|
||||
expectedChartsCount: number
|
||||
) {
|
||||
await this.assertBasicCaseProps(params);
|
||||
await testSubjects.existOrFail('comment-persistableState-ml_anomaly_charts');
|
||||
|
||||
await mlAnomalyCharts.assertAnomalyExplorerChartsCount(
|
||||
`mlExplorerEmbeddable_${attachment.id}`,
|
||||
expectedChartsCount
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import { TrainedModelsTableProvider } from './trained_models_table';
|
|||
import { MachineLearningJobAnnotationsProvider } from './job_annotations_table';
|
||||
import { MlNodesPanelProvider } from './ml_nodes_list';
|
||||
import { MachineLearningCasesProvider } from './cases';
|
||||
import { AnomalyChartsProvider } from './anomaly_charts';
|
||||
|
||||
export function MachineLearningProvider(context: FtrProviderContext) {
|
||||
const commonAPI = MachineLearningCommonAPIProvider(context);
|
||||
|
@ -65,7 +66,8 @@ export function MachineLearningProvider(context: FtrProviderContext) {
|
|||
const commonDataGrid = MachineLearningCommonDataGridProvider(context);
|
||||
|
||||
const anomaliesTable = MachineLearningAnomaliesTableProvider(context);
|
||||
const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context);
|
||||
const anomalyCharts = AnomalyChartsProvider(context);
|
||||
const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context, anomalyCharts);
|
||||
const api = MachineLearningAPIProvider(context);
|
||||
const commonConfig = MachineLearningCommonConfigsProvider(context);
|
||||
const customUrls = MachineLearningCustomUrlsProvider(context);
|
||||
|
@ -129,10 +131,11 @@ export function MachineLearningProvider(context: FtrProviderContext) {
|
|||
const trainedModelsTable = TrainedModelsTableProvider(context, commonUI);
|
||||
const mlNodesPanel = MlNodesPanelProvider(context);
|
||||
|
||||
const cases = MachineLearningCasesProvider(context, swimLane);
|
||||
const cases = MachineLearningCasesProvider(context, swimLane, anomalyCharts);
|
||||
|
||||
return {
|
||||
anomaliesTable,
|
||||
anomalyCharts,
|
||||
anomalyExplorer,
|
||||
alerting,
|
||||
api,
|
||||
|
|
|
@ -150,12 +150,19 @@ export function MachineLearningNavigationProvider({
|
|||
await this.navigateToArea('~mlMainTab & ~anomalyDetection', 'mlPageJobManagement');
|
||||
},
|
||||
|
||||
async navigateToAnomalyExplorer(jobId: string, timeRange: { from: string; to: string }) {
|
||||
async navigateToAnomalyExplorer(
|
||||
jobId: string,
|
||||
timeRange: { from: string; to: string },
|
||||
callback?: () => Promise<void>
|
||||
) {
|
||||
await PageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'ml',
|
||||
`/explorer`,
|
||||
`?_g=(ml%3A(jobIds%3A!(${jobId}))%2CrefreshInterval%3A(display%3AOff%2Cpause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3A'${timeRange.from}'%2Cto%3A'${timeRange.to}'))`
|
||||
);
|
||||
if (callback) {
|
||||
await callback();
|
||||
}
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue