mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] Anomaly Detection: Single Metric Viewer - add cases action (#183423)
## Summary Related meta issue https://github.com/elastic/kibana/issues/181272 This PR adds the 'Add to case' action in the Single Metric Viewer ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dima Arnautov <arnautov.dima@gmail.com>
This commit is contained in:
parent
2c7efb4464
commit
2ec4ec362d
19 changed files with 563 additions and 222 deletions
|
@ -7,3 +7,4 @@
|
|||
|
||||
export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE = 'ml_anomaly_swimlane' as const;
|
||||
export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS = 'ml_anomaly_charts' as const;
|
||||
export const CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER = 'ml_single_metric_viewer' as const;
|
||||
|
|
|
@ -24,8 +24,10 @@ import {
|
|||
LazySavedObjectSaveModalDashboard,
|
||||
withSuspense,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import type { JobId } from '../../../../../common/types/anomaly_detection_jobs/job';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import { useCasesModal } from '../../../contexts/kibana/use_cases_modal';
|
||||
import { getDefaultSingleMetricViewerPanelTitle } from '../../../../embeddables/single_metric_viewer/get_default_panel_title';
|
||||
import type { MlEntity } from '../../../../embeddables';
|
||||
import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '../../../../embeddables/constants';
|
||||
|
@ -77,10 +79,13 @@ export const TimeSeriesExplorerControls: FC<Props> = ({
|
|||
const {
|
||||
services: {
|
||||
application: { capabilities },
|
||||
cases,
|
||||
embeddable,
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const globalTimeRange = useTimeRangeUpdates(true);
|
||||
|
||||
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
|
||||
|
||||
const closePopoverOnAction = useCallback(
|
||||
|
@ -93,6 +98,8 @@ export const TimeSeriesExplorerControls: FC<Props> = ({
|
|||
[setIsMenuOpen]
|
||||
);
|
||||
|
||||
const openCasesModalCallback = useCasesModal(ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE);
|
||||
|
||||
const menuPanels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
|
@ -112,6 +119,27 @@ export const TimeSeriesExplorerControls: FC<Props> = ({
|
|||
},
|
||||
];
|
||||
|
||||
const casesPrivileges = cases?.helpers.canUseCases();
|
||||
|
||||
if (!!casesPrivileges?.create || !!casesPrivileges?.update) {
|
||||
menuPanels[0].items!.push({
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeseriesExplorer.addToCaseLabel"
|
||||
defaultMessage="Add to case"
|
||||
/>
|
||||
),
|
||||
onClick: closePopoverOnAction(() => {
|
||||
openCasesModalCallback({
|
||||
jobIds: [selectedJobId],
|
||||
selectedDetectorIndex,
|
||||
selectedEntities,
|
||||
timeRange: globalTimeRange,
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback(
|
||||
({ dashboardId, newTitle, newDescription }) => {
|
||||
const stateTransfer = embeddable!.getStateTransfer();
|
||||
|
|
|
@ -79,6 +79,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component {
|
|||
bounds: PropTypes.object.isRequired,
|
||||
chartWidth: PropTypes.number.isRequired,
|
||||
lastRefresh: PropTypes.number.isRequired,
|
||||
onRenderComplete: PropTypes.func,
|
||||
previousRefresh: PropTypes.number.isRequired,
|
||||
selectedJobId: PropTypes.string.isRequired,
|
||||
selectedDetectorIndex: PropTypes.number,
|
||||
|
@ -434,6 +435,9 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component {
|
|||
}
|
||||
|
||||
this.setState(stateUpdate);
|
||||
if (this.props.onRenderComplete !== undefined) {
|
||||
this.props.onRenderComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -33,9 +33,11 @@ export const TimeSeriesExplorerPage: FC<PropsWithChildren<TimeSeriesExplorerPage
|
|||
noSingleMetricJobsFound,
|
||||
}) => {
|
||||
const {
|
||||
services: { presentationUtil, docLinks },
|
||||
services: { cases, docLinks, presentationUtil },
|
||||
} = useMlKibana();
|
||||
const PresentationContextProvider = presentationUtil?.ContextProvider ?? React.Fragment;
|
||||
const CasesContext = cases?.ui.getCasesContext() ?? React.Fragment;
|
||||
const casesPermissions = cases?.helpers.canUseCases();
|
||||
const helpLink = docLinks.links.ml.anomalyDetection;
|
||||
return (
|
||||
<>
|
||||
|
@ -62,7 +64,9 @@ export const TimeSeriesExplorerPage: FC<PropsWithChildren<TimeSeriesExplorerPage
|
|||
{noSingleMetricJobsFound ? null : (
|
||||
<JobSelector dateFormatTz={dateFormatTz!} singleSelection={true} timeseriesOnly={true} />
|
||||
)}
|
||||
<PresentationContextProvider>{children}</PresentationContextProvider>
|
||||
<CasesContext owner={[]} permissions={casesPermissions!}>
|
||||
<PresentationContextProvider>{children}</PresentationContextProvider>
|
||||
</CasesContext>
|
||||
<HelpMenu docLink={helpLink} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -8,14 +8,23 @@
|
|||
import type { CasesPublicSetup } from '@kbn/cases-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { registerAnomalyChartsCasesAttachment } from './register_anomaly_charts_attachment';
|
||||
import { registerSingleMetricViewerCasesAttachment } from './register_single_metric_viewer_attachment';
|
||||
import type { MlStartDependencies } from '../plugin';
|
||||
import type { SingleMetricViewerServices } from '../embeddables/types';
|
||||
import { registerAnomalySwimLaneCasesAttachment } from './register_anomaly_swim_lane_attachment';
|
||||
|
||||
export function registerCasesAttachments(
|
||||
cases: CasesPublicSetup,
|
||||
coreStart: CoreStart,
|
||||
pluginStart: MlStartDependencies
|
||||
pluginStart: MlStartDependencies,
|
||||
singleMetricViewerServices: SingleMetricViewerServices
|
||||
) {
|
||||
registerAnomalySwimLaneCasesAttachment(cases, pluginStart);
|
||||
registerAnomalyChartsCasesAttachment(cases, coreStart, pluginStart);
|
||||
registerSingleMetricViewerCasesAttachment(
|
||||
cases,
|
||||
coreStart,
|
||||
pluginStart,
|
||||
singleMetricViewerServices
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 type { CasesPublicSetup } from '@kbn/cases-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { PLUGIN_ICON } from '../../common/constants/app';
|
||||
import { CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER } from '../../common/constants/cases';
|
||||
import type { MlStartDependencies } from '../plugin';
|
||||
import { getSingleMetricViewerComponent } from '../shared_components/single_metric_viewer';
|
||||
import type { SingleMetricViewerServices } from '../embeddables/types';
|
||||
import type { MlDependencies } from '../application/app';
|
||||
|
||||
export function registerSingleMetricViewerCasesAttachment(
|
||||
cases: CasesPublicSetup,
|
||||
coreStart: CoreStart,
|
||||
pluginStart: MlStartDependencies,
|
||||
mlServices: SingleMetricViewerServices
|
||||
) {
|
||||
const SingleMetricViewerComponent = getSingleMetricViewerComponent(
|
||||
coreStart,
|
||||
pluginStart as MlDependencies,
|
||||
mlServices
|
||||
);
|
||||
|
||||
cases.attachmentFramework.registerPersistableState({
|
||||
id: CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER,
|
||||
icon: PLUGIN_ICON,
|
||||
displayName: i18n.translate('xpack.ml.cases.registerSingleMetricViewer.displayName', {
|
||||
defaultMessage: 'Single metric viewer',
|
||||
}),
|
||||
getAttachmentViewObject: () => ({
|
||||
event: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.singleMetricViewer.embeddableAddedEvent"
|
||||
defaultMessage="added single metric viewer"
|
||||
/>
|
||||
),
|
||||
timelineAvatar: PLUGIN_ICON,
|
||||
children: React.lazy(async () => {
|
||||
const { initComponent } = await import('./single_metric_viewer_attachment');
|
||||
return {
|
||||
default: initComponent(pluginStart.fieldFormats, SingleMetricViewerComponent),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { EuiDescriptionList } from '@elastic/eui';
|
||||
import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
import moment from 'moment';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { memoize } from 'lodash';
|
||||
import React from 'react';
|
||||
import type { SingleMetricViewerEmbeddableState } from '../embeddables/types';
|
||||
import type { SingleMetricViewerSharedComponent } from '../shared_components/single_metric_viewer';
|
||||
|
||||
export const initComponent = memoize(
|
||||
(
|
||||
fieldFormats: FieldFormatsStart,
|
||||
SingleMetricViewerComponent: SingleMetricViewerSharedComponent
|
||||
) => {
|
||||
return React.memo(
|
||||
(props: PersistableStateAttachmentViewProps) => {
|
||||
const { persistableStateAttachmentState, caseData } = props;
|
||||
|
||||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as SingleMetricViewerEmbeddableState;
|
||||
|
||||
const dataFormatter = fieldFormats.deserialize({
|
||||
id: FIELD_FORMAT_IDS.DATE,
|
||||
});
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.singleMetricViewer.description.jobIdLabel"
|
||||
defaultMessage="Job ID"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.singleMetricViewer.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange!.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange!.to)}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof inputProps.query?.query === 'string' && inputProps.query?.query !== '') {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.singleMetricViewer.description.queryLabel"
|
||||
defaultMessage="Query"
|
||||
/>
|
||||
),
|
||||
description: inputProps.query?.query,
|
||||
});
|
||||
}
|
||||
|
||||
const { jobIds, timeRange, ...rest } = inputProps;
|
||||
const selectedJobId = jobIds[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList compressed type={'inline'} listItems={listItems} />
|
||||
<SingleMetricViewerComponent
|
||||
bounds={{ min: moment(timeRange!.from), max: moment(timeRange!.to) }}
|
||||
lastRefresh={Date.now()}
|
||||
selectedJobId={selectedJobId}
|
||||
uuid={caseData.id}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
deepEqual(
|
||||
prevProps.persistableStateAttachmentState,
|
||||
nextProps.persistableStateAttachmentState
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
|
@ -14,4 +14,7 @@ export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS
|
|||
export type AnomalySingleMetricViewerEmbeddableType =
|
||||
typeof ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE;
|
||||
|
||||
export type MlEmbeddableTypes = AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType;
|
||||
export type MlEmbeddableTypes =
|
||||
| AnomalySwimLaneEmbeddableType
|
||||
| AnomalyExplorerChartsEmbeddableType
|
||||
| AnomalySingleMetricViewerEmbeddableType;
|
||||
|
|
|
@ -6,20 +6,21 @@
|
|||
*/
|
||||
|
||||
import type { StartServicesAccessor } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
|
||||
import type { MlDependencies } from '../../application/app';
|
||||
import { HttpService } from '../../application/services/http_service';
|
||||
import { AnomalyExplorerChartsService } from '../../application/services/anomaly_explorer_charts_service';
|
||||
import type { SingleMetricViewerEmbeddableServices } from '../types';
|
||||
import type { SingleMetricViewerEmbeddableServices, SingleMetricViewerServices } from '../types';
|
||||
|
||||
/**
|
||||
* Provides the services required by the Anomaly Swimlane Embeddable.
|
||||
* Provides the ML services required by the Single Metric Viewer Embeddable.
|
||||
*/
|
||||
export const getServices = async (
|
||||
getStartServices: StartServicesAccessor<MlStartDependencies, MlPluginStart>
|
||||
): Promise<SingleMetricViewerEmbeddableServices> => {
|
||||
export const getMlServices = async (
|
||||
coreStart: CoreStart,
|
||||
pluginsStart: MlStartDependencies
|
||||
): Promise<SingleMetricViewerServices> => {
|
||||
const [
|
||||
[coreStart, pluginsStart],
|
||||
{ AnomalyDetectorService },
|
||||
{ fieldFormatServiceFactory },
|
||||
{ indexServiceFactory },
|
||||
|
@ -31,7 +32,6 @@ export const getServices = async (
|
|||
{ timeSeriesSearchServiceFactory },
|
||||
{ toastNotificationServiceProvider },
|
||||
] = await Promise.all([
|
||||
await getStartServices(),
|
||||
await import('../../application/services/anomaly_detector_service'),
|
||||
await import('../../application/services/field_format_service_factory'),
|
||||
await import('../../application/util/index_service'),
|
||||
|
@ -64,7 +64,6 @@ export const getServices = async (
|
|||
mlApiServices,
|
||||
mlResultsService
|
||||
);
|
||||
|
||||
// Note on the following services:
|
||||
// - `mlIndexUtils` is just instantiated here to be passed on to `mlFieldFormatService`,
|
||||
// but it's not being made available as part of global services. Since it's just
|
||||
|
@ -76,21 +75,28 @@ export const getServices = async (
|
|||
// way this manages its own state right now doesn't consider React component lifecycles.
|
||||
const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews);
|
||||
const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils);
|
||||
|
||||
return [
|
||||
coreStart,
|
||||
pluginsStart as MlDependencies,
|
||||
{
|
||||
anomalyDetectorService,
|
||||
anomalyExplorerService,
|
||||
mlApiServices,
|
||||
mlCapabilities,
|
||||
mlFieldFormatService,
|
||||
mlJobService,
|
||||
mlResultsService,
|
||||
mlTimeSeriesSearchService,
|
||||
mlTimeSeriesExplorerService,
|
||||
toastNotificationService,
|
||||
},
|
||||
];
|
||||
return {
|
||||
anomalyDetectorService,
|
||||
anomalyExplorerService,
|
||||
mlApiServices,
|
||||
mlCapabilities,
|
||||
mlFieldFormatService,
|
||||
mlJobService,
|
||||
mlResultsService,
|
||||
mlTimeSeriesSearchService,
|
||||
mlTimeSeriesExplorerService,
|
||||
toastNotificationService,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the services required by the Single Metric Viewer Embeddable.
|
||||
*/
|
||||
export const getServices = async (
|
||||
getStartServices: StartServicesAccessor<MlStartDependencies, MlPluginStart>
|
||||
): Promise<SingleMetricViewerEmbeddableServices> => {
|
||||
const [coreStart, pluginsStart] = await getStartServices();
|
||||
const mlServices = await getMlServices(coreStart, pluginsStart);
|
||||
|
||||
return [coreStart, pluginsStart as MlDependencies, mlServices];
|
||||
};
|
||||
|
|
|
@ -52,8 +52,12 @@ export const initializeSingleMetricViewerDataFetcher = (
|
|||
([singleMetricViewerData, fetchContext]) => {
|
||||
let bounds;
|
||||
let lastRefresh;
|
||||
if (timefilter !== undefined && fetchContext.timeRange !== undefined) {
|
||||
bounds = timefilter.calculateBounds(fetchContext.timeRange);
|
||||
if (timefilter !== undefined) {
|
||||
bounds = timefilter.calculateBounds(
|
||||
fetchContext?.timeRange
|
||||
? fetchContext?.timeRange
|
||||
: api.timeRange$?.value ?? timefilter.getTime()
|
||||
);
|
||||
lastRefresh = Date.now();
|
||||
}
|
||||
singleMetricViewerData$.next({ singleMetricViewerData, bounds, lastRefresh });
|
||||
|
|
|
@ -6,27 +6,17 @@
|
|||
*/
|
||||
|
||||
import type { StartServicesAccessor } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { pick } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
|
||||
import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { EuiResizeObserver } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
apiHasExecutionContext,
|
||||
initializeTimeRange,
|
||||
initializeTitles,
|
||||
useStateFromPublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import type { MlJob } from '@elastic/elasticsearch/lib/api/types';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { throttle } from 'lodash';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..';
|
||||
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
|
||||
|
@ -37,23 +27,9 @@ import type {
|
|||
} from '../types';
|
||||
import { initializeSingleMetricViewerControls } from './single_metric_viewer_controls_initializer';
|
||||
import { initializeSingleMetricViewerDataFetcher } from './single_metric_viewer_data_fetcher';
|
||||
import { TimeSeriesExplorerEmbeddableChart } from '../../application/timeseriesexplorer/timeseriesexplorer_embeddable_chart';
|
||||
import { APP_STATE_ACTION } from '../../application/timeseriesexplorer/timeseriesexplorer_constants';
|
||||
import { getServices } from './get_services';
|
||||
import { useReactEmbeddableExecutionContext } from '../common/use_embeddable_execution_context';
|
||||
import './_index.scss';
|
||||
|
||||
const RESIZE_THROTTLE_TIME_MS = 500;
|
||||
const containerPadding = 10;
|
||||
const minElemAndChartDiff = 20;
|
||||
interface AppStateZoom {
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
const errorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', {
|
||||
defaultMessage: 'Unable to load the ML single metric viewer data',
|
||||
});
|
||||
import { getSingleMetricViewerComponent } from '../../shared_components/single_metric_viewer';
|
||||
|
||||
export const getSingleMetricViewerEmbeddableFactory = (
|
||||
getStartServices: StartServicesAccessor<MlStartDependencies, MlPluginStart>
|
||||
|
@ -147,31 +123,11 @@ export const getSingleMetricViewerEmbeddableFactory = (
|
|||
services[1].data.query.timefilter.timefilter
|
||||
);
|
||||
|
||||
const SingleMetricViewerComponent = getSingleMetricViewerComponent(...services);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||
const [zoom, setZoom] = useState<AppStateZoom | undefined>();
|
||||
const [selectedForecastId, setSelectedForecastId] = useState<string | undefined>();
|
||||
const [selectedJob, setSelectedJob] = useState<MlJob | undefined>();
|
||||
const [jobsLoaded, setJobsLoaded] = useState(false);
|
||||
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const {
|
||||
mlApiServices,
|
||||
mlJobService,
|
||||
mlTimeSeriesExplorerService,
|
||||
toastNotificationService,
|
||||
} = services[2];
|
||||
const startServices = pick(services[0], 'analytics', 'i18n', 'theme');
|
||||
const datePickerDeps: DatePickerDependencies = {
|
||||
...pick(services[0], ['http', 'notifications', 'theme', 'uiSettings', 'i18n']),
|
||||
data: services[1].data,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
showFrozenDataTierChoice: false,
|
||||
};
|
||||
|
||||
if (!apiHasExecutionContext(parentApi)) {
|
||||
throw new Error('Parent API does not have execution context');
|
||||
}
|
||||
|
@ -192,153 +148,27 @@ export const getSingleMetricViewerEmbeddableFactory = (
|
|||
subscriptions.unsubscribe();
|
||||
});
|
||||
|
||||
const selectedJobId = singleMetricViewerData?.jobIds[0];
|
||||
// Need to make sure we fall back to `undefined` if `functionDescription` is an empty string,
|
||||
// otherwise anomaly table data will not be loaded.
|
||||
const functionDescription =
|
||||
(singleMetricViewerData?.functionDescription ?? '') === ''
|
||||
? undefined
|
||||
: singleMetricViewerData?.functionDescription;
|
||||
const previousRefresh = usePrevious(lastRefresh ?? 0);
|
||||
|
||||
useEffect(function setUpJobsLoaded() {
|
||||
async function loadJobs() {
|
||||
try {
|
||||
await mlJobService.loadJobsWrapper();
|
||||
setJobsLoaded(true);
|
||||
} catch (e) {
|
||||
blockingError.next(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
if (isMounted.current === false) {
|
||||
return;
|
||||
}
|
||||
loadJobs();
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
function setUpSelectedJob() {
|
||||
async function fetchSelectedJob() {
|
||||
if (mlApiServices && selectedJobId !== undefined) {
|
||||
try {
|
||||
const { jobs } = await mlApiServices.getJobs({ jobId: selectedJobId });
|
||||
const job = jobs[0];
|
||||
setSelectedJob(job);
|
||||
} catch (e) {
|
||||
blockingError.next(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMounted.current === false) {
|
||||
return;
|
||||
}
|
||||
fetchSelectedJob();
|
||||
},
|
||||
[selectedJobId, mlApiServices]
|
||||
);
|
||||
|
||||
const autoZoomDuration = useMemo(() => {
|
||||
if (!selectedJob) return;
|
||||
return mlTimeSeriesExplorerService?.getAutoZoomDuration(selectedJob);
|
||||
}, [mlTimeSeriesExplorerService, selectedJob]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const resizeHandler = useCallback(
|
||||
throttle((e: { width: number; height: number }) => {
|
||||
if (Math.abs(chartWidth - e.width) > minElemAndChartDiff) {
|
||||
setChartWidth(e.width);
|
||||
}
|
||||
}, RESIZE_THROTTLE_TIME_MS),
|
||||
[chartWidth]
|
||||
);
|
||||
|
||||
const appStateHandler = useCallback(
|
||||
(action: string, payload?: any) => {
|
||||
/**
|
||||
* Empty zoom indicates that chart hasn't been rendered yet,
|
||||
* hence any updates prior that should replace the URL state.
|
||||
*/
|
||||
switch (action) {
|
||||
case APP_STATE_ACTION.SET_FORECAST_ID:
|
||||
setSelectedForecastId(payload);
|
||||
setZoom(undefined);
|
||||
break;
|
||||
|
||||
case APP_STATE_ACTION.SET_ZOOM:
|
||||
setZoom(payload);
|
||||
break;
|
||||
|
||||
case APP_STATE_ACTION.UNSET_ZOOM:
|
||||
setZoom(undefined);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
[setZoom, setSelectedForecastId]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaRenderContextProvider {...startServices}>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
mlServices: {
|
||||
...services[2],
|
||||
},
|
||||
...services[0],
|
||||
...services[1],
|
||||
}}
|
||||
>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<EuiResizeObserver onResize={resizeHandler}>
|
||||
{(resizeRef) => (
|
||||
<div
|
||||
id={`mlSingleMetricViewerEmbeddableWrapper-${api.uuid}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: '8px',
|
||||
}}
|
||||
data-test-subj={`mlSingleMetricViewer_${api.uuid}`}
|
||||
ref={resizeRef}
|
||||
className="ml-time-series-explorer"
|
||||
>
|
||||
{singleMetricViewerData !== undefined &&
|
||||
autoZoomDuration !== undefined &&
|
||||
jobsLoaded &&
|
||||
selectedJobId === selectedJob?.job_id && (
|
||||
<TimeSeriesExplorerEmbeddableChart
|
||||
chartWidth={chartWidth - containerPadding}
|
||||
dataViewsService={services[1].data.dataViews}
|
||||
toastNotificationService={toastNotificationService}
|
||||
appStateHandler={appStateHandler}
|
||||
autoZoomDuration={autoZoomDuration}
|
||||
bounds={bounds}
|
||||
dateFormatTz={moment.tz.guess()}
|
||||
lastRefresh={lastRefresh ?? 0}
|
||||
previousRefresh={previousRefresh}
|
||||
selectedJobId={selectedJobId}
|
||||
selectedDetectorIndex={singleMetricViewerData.selectedDetectorIndex}
|
||||
selectedEntities={singleMetricViewerData.selectedEntities}
|
||||
selectedForecastId={selectedForecastId}
|
||||
tableInterval="auto"
|
||||
tableSeverity={0}
|
||||
zoom={zoom}
|
||||
functionDescription={functionDescription}
|
||||
selectedJob={selectedJob}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</EuiResizeObserver>
|
||||
</DatePickerContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
<SingleMetricViewerComponent
|
||||
bounds={bounds}
|
||||
functionDescription={functionDescription}
|
||||
lastRefresh={lastRefresh}
|
||||
onError={(error) => blockingError.next(error)}
|
||||
selectedDetectorIndex={singleMetricViewerData?.selectedDetectorIndex}
|
||||
selectedEntities={singleMetricViewerData?.selectedEntities}
|
||||
selectedJobId={singleMetricViewerData?.jobIds[0]}
|
||||
uuid={api.uuid}
|
||||
onRenderComplete={() => {
|
||||
dataLoading.next(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -47,13 +47,13 @@ export async function resolveEmbeddableSingleMetricViewerUserInput(
|
|||
bounds={timefilter.getBounds()!}
|
||||
initialInput={input}
|
||||
onCreate={(explicitInput) => {
|
||||
flyoutSession.close();
|
||||
resolve(explicitInput);
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
}}
|
||||
onCancel={() => {
|
||||
flyoutSession.close();
|
||||
reject();
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
}}
|
||||
/>
|
||||
|
@ -65,8 +65,9 @@ export async function resolveEmbeddableSingleMetricViewerUserInput(
|
|||
ownFocus: true,
|
||||
size: 's',
|
||||
onClose: () => {
|
||||
flyoutSession.close();
|
||||
reject();
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -73,6 +73,7 @@ import type { ElasticModels } from './application/services/elastic_models_servic
|
|||
import type { MlApiServices } from './application/services/ml_api_service';
|
||||
import type { MlCapabilities } from '../common/types/capabilities';
|
||||
import { AnomalySwimLane } from './shared_components';
|
||||
import { getMlServices } from './embeddables/single_metric_viewer/get_services';
|
||||
|
||||
export interface MlStartDependencies {
|
||||
cases?: CasesPublicStart;
|
||||
|
@ -272,7 +273,8 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
registerEmbeddables(pluginsSetup.embeddable, core);
|
||||
|
||||
if (pluginsSetup.cases) {
|
||||
registerCasesAttachments(pluginsSetup.cases, coreStart, pluginStart);
|
||||
const mlServices = await getMlServices(coreStart, pluginStart);
|
||||
registerCasesAttachments(pluginsSetup.cases, coreStart, pluginStart, mlServices);
|
||||
}
|
||||
|
||||
if (pluginsSetup.maps) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { dynamic } from '@kbn/shared-ux-utility';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { SingleMetricViewerServices } from '../../embeddables/types';
|
||||
import type { MlDependencies } from '../../application/app';
|
||||
import type { SingleMetricViewerSharedComponent } from './single_metric_viewer';
|
||||
|
||||
const SingleMetricViewerLazy = dynamic(async () => import('./single_metric_viewer'));
|
||||
|
||||
export const getSingleMetricViewerComponent = (
|
||||
coreStart: CoreStart,
|
||||
pluginStart: MlDependencies,
|
||||
mlServices: SingleMetricViewerServices
|
||||
): SingleMetricViewerSharedComponent => {
|
||||
return (props) => {
|
||||
return (
|
||||
<SingleMetricViewerLazy
|
||||
coreStart={coreStart}
|
||||
pluginStart={pluginStart}
|
||||
mlServices={mlServices}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export type { SingleMetricViewerSharedComponent } from './single_metric_viewer';
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* 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, useEffect, useMemo, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiResizeObserver } from '@elastic/eui';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import type { MlJob } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
||||
import type { TimeRangeBounds } from '@kbn/ml-time-buckets';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { tz } from 'moment';
|
||||
import { pick, throttle } from 'lodash';
|
||||
import type { MlDependencies } from '../../application/app';
|
||||
import { TimeSeriesExplorerEmbeddableChart } from '../../application/timeseriesexplorer/timeseriesexplorer_embeddable_chart';
|
||||
import { APP_STATE_ACTION } from '../../application/timeseriesexplorer/timeseriesexplorer_constants';
|
||||
import type { SingleMetricViewerServices, MlEntity } from '../../embeddables/types';
|
||||
import './_index.scss';
|
||||
|
||||
const containerPadding = 10;
|
||||
const minElemAndChartDiff = 20;
|
||||
const RESIZE_THROTTLE_TIME_MS = 500;
|
||||
interface AppStateZoom {
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
const errorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', {
|
||||
defaultMessage: 'Unable to load the ML single metric viewer data',
|
||||
});
|
||||
|
||||
export type SingleMetricViewerSharedComponent = FC<SingleMetricViewerProps>;
|
||||
|
||||
/**
|
||||
* Only used to initialize internally
|
||||
*/
|
||||
export type SingleMetricViewerPropsWithDeps = SingleMetricViewerProps & {
|
||||
coreStart: CoreStart;
|
||||
pluginStart: MlDependencies;
|
||||
mlServices: SingleMetricViewerServices;
|
||||
};
|
||||
|
||||
export interface SingleMetricViewerProps {
|
||||
bounds?: TimeRangeBounds;
|
||||
selectedEntities?: MlEntity;
|
||||
selectedDetectorIndex?: number;
|
||||
functionDescription?: string;
|
||||
selectedJobId: string | undefined;
|
||||
/**
|
||||
* Last reload request time, can be used for manual reload
|
||||
*/
|
||||
lastRefresh?: number;
|
||||
onRenderComplete?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
type Zoom = AppStateZoom | undefined;
|
||||
type ForecastId = string | undefined;
|
||||
|
||||
const SingleMetricViewerWrapper: FC<SingleMetricViewerPropsWithDeps> = ({
|
||||
// Component dependencies
|
||||
coreStart,
|
||||
pluginStart,
|
||||
mlServices,
|
||||
// Component props
|
||||
bounds,
|
||||
functionDescription,
|
||||
lastRefresh,
|
||||
onError,
|
||||
onRenderComplete,
|
||||
selectedDetectorIndex,
|
||||
selectedEntities,
|
||||
selectedJobId,
|
||||
uuid,
|
||||
}) => {
|
||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||
const [zoom, setZoom] = useState<Zoom>();
|
||||
const [selectedForecastId, setSelectedForecastId] = useState<ForecastId>();
|
||||
const [selectedJob, setSelectedJob] = useState<MlJob | undefined>();
|
||||
const [jobsLoaded, setJobsLoaded] = useState(false);
|
||||
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const { mlApiServices, mlJobService, mlTimeSeriesExplorerService, toastNotificationService } =
|
||||
mlServices;
|
||||
const startServices = pick(coreStart, 'analytics', 'i18n', 'theme');
|
||||
const datePickerDeps: DatePickerDependencies = {
|
||||
...pick(coreStart, ['http', 'notifications', 'theme', 'uiSettings', 'i18n']),
|
||||
data: pluginStart.data,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
showFrozenDataTierChoice: false,
|
||||
};
|
||||
|
||||
const previousRefresh = usePrevious(lastRefresh ?? 0);
|
||||
|
||||
useEffect(
|
||||
function setUpJobsLoaded() {
|
||||
async function loadJobs() {
|
||||
try {
|
||||
await mlJobService.loadJobsWrapper();
|
||||
setJobsLoaded(true);
|
||||
} catch (e) {
|
||||
if (onError) {
|
||||
onError(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMounted() === false) {
|
||||
return;
|
||||
}
|
||||
loadJobs();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isMounted]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function setUpSelectedJob() {
|
||||
async function fetchSelectedJob() {
|
||||
if (mlApiServices && selectedJobId !== undefined) {
|
||||
try {
|
||||
const { jobs } = await mlApiServices.getJobs({ jobId: selectedJobId });
|
||||
const job = jobs[0];
|
||||
setSelectedJob(job);
|
||||
} catch (e) {
|
||||
if (onError) {
|
||||
onError(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMounted() === false) {
|
||||
return;
|
||||
}
|
||||
fetchSelectedJob();
|
||||
},
|
||||
[selectedJobId, mlApiServices, isMounted, onError]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const resizeHandler = useCallback(
|
||||
throttle((e: { width: number; height: number }) => {
|
||||
if (Math.abs(chartWidth - e.width) > minElemAndChartDiff) {
|
||||
setChartWidth(e.width);
|
||||
}
|
||||
}, RESIZE_THROTTLE_TIME_MS),
|
||||
[chartWidth]
|
||||
);
|
||||
|
||||
const autoZoomDuration = useMemo(() => {
|
||||
if (!selectedJob) return;
|
||||
return mlTimeSeriesExplorerService?.getAutoZoomDuration(selectedJob);
|
||||
}, [mlTimeSeriesExplorerService, selectedJob]);
|
||||
|
||||
const appStateHandler = useCallback(
|
||||
(action: string, payload?: Zoom | ForecastId) => {
|
||||
/**
|
||||
* Empty zoom indicates that chart hasn't been rendered yet,
|
||||
* hence any updates prior that should replace the URL state.
|
||||
*/
|
||||
switch (action) {
|
||||
case APP_STATE_ACTION.SET_FORECAST_ID:
|
||||
setSelectedForecastId(payload as ForecastId);
|
||||
setZoom(undefined);
|
||||
break;
|
||||
|
||||
case APP_STATE_ACTION.SET_ZOOM:
|
||||
setZoom(payload as Zoom);
|
||||
break;
|
||||
|
||||
case APP_STATE_ACTION.UNSET_ZOOM:
|
||||
setZoom(undefined);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
[setZoom, setSelectedForecastId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiResizeObserver onResize={resizeHandler}>
|
||||
{(resizeRef) => (
|
||||
<div
|
||||
id={`mlSingleMetricViewerEmbeddableWrapper-${uuid}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: '8px',
|
||||
}}
|
||||
data-test-subj={`mlSingleMetricViewer_${uuid}`}
|
||||
ref={resizeRef}
|
||||
className="ml-time-series-explorer"
|
||||
data-shared-item="" // TODO: Remove data-shared-item as part of https://github.com/elastic/kibana/issues/179376
|
||||
data-rendering-count={1}
|
||||
>
|
||||
<KibanaRenderContextProvider {...startServices}>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
mlServices: {
|
||||
...mlServices,
|
||||
},
|
||||
...coreStart,
|
||||
...pluginStart,
|
||||
}}
|
||||
>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
{selectedJobId !== undefined &&
|
||||
autoZoomDuration !== undefined &&
|
||||
jobsLoaded &&
|
||||
selectedJobId === selectedJob?.job_id && (
|
||||
<TimeSeriesExplorerEmbeddableChart
|
||||
chartWidth={chartWidth - containerPadding}
|
||||
dataViewsService={pluginStart.data.dataViews}
|
||||
toastNotificationService={toastNotificationService}
|
||||
appStateHandler={appStateHandler}
|
||||
autoZoomDuration={autoZoomDuration}
|
||||
bounds={bounds}
|
||||
dateFormatTz={tz.guess()}
|
||||
lastRefresh={lastRefresh ?? 0}
|
||||
previousRefresh={previousRefresh}
|
||||
selectedJobId={selectedJobId}
|
||||
selectedDetectorIndex={selectedDetectorIndex}
|
||||
selectedEntities={selectedEntities}
|
||||
selectedForecastId={selectedForecastId}
|
||||
tableInterval="auto"
|
||||
tableSeverity={0}
|
||||
zoom={zoom}
|
||||
functionDescription={functionDescription}
|
||||
selectedJob={selectedJob}
|
||||
onRenderComplete={onRenderComplete}
|
||||
/>
|
||||
)}
|
||||
</DatePickerContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
</div>
|
||||
)}
|
||||
</EuiResizeObserver>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default SingleMetricViewerWrapper;
|
|
@ -11,6 +11,7 @@ import type { MlFeatures } from '../../common/constants/app';
|
|||
import {
|
||||
CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS,
|
||||
CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE,
|
||||
CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER,
|
||||
} from '../../common/constants/cases';
|
||||
|
||||
export function registerCasesPersistableState(
|
||||
|
@ -37,5 +38,15 @@ export function registerCasesPersistableState(
|
|||
`ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
cases.attachmentFramework.registerPersistableState({
|
||||
id: CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,5 +126,6 @@
|
|||
"@kbn/shared-ux-utility",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c',
|
||||
ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147',
|
||||
ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f',
|
||||
ml_single_metric_viewer: '8b9532b0a40dfdfa282e262949b82cc1a643147c',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue