Skip to content

Commit

Permalink
[ML] Single Metric Viewer embeddable: add open and edit panel actions (
Browse files Browse the repository at this point in the history
…#179364)

## Summary

Part of #176651

This PR:

- adds the 'Open in Single Metric Viewer' and 'Edit' panel actions to
the Single Metric Viewer embeddable for dashboards.
- detector index fix moved to separate
[PR](#179761)




### Screen shots ###

<img width="672" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/4955143f-ace0-406f-839b-8f2469d3ea2f">




### 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 <[email protected]>
  • Loading branch information
alvarezmelissa87 and kibanamachine authored Apr 4, 2024
1 parent c984671 commit 339e24f
Show file tree
Hide file tree
Showing 22 changed files with 340 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ class AnnotationsTableUI extends Component {
entityCondition[annotation.by_field_name] = annotation.by_field_value;
}
mlTimeSeriesExplorer.entities = entityCondition;
// appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer;

const mlLocator = share.url.locators.get(ML_APP_LOCATOR);
const singleMetricViewerLink = await mlLocator.getUrl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type { FC } from 'react';
import React, { useState, useMemo } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiTitle,
Expand All @@ -24,7 +24,6 @@ import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana';
import { useDataSource } from '../../../../contexts/ml';
import { DataRecognizer } from '../../../../components/data_recognizer';
import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed';
import { timeBasedIndexCheck } from '../../../../util/index_utils';
import { LinkCard } from '../../../../components/link_card';
import { CategorizationIcon } from './categorization_job_icon';
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../../common/constants/locator';
Expand All @@ -35,7 +34,10 @@ import { MlPageHeader } from '../../../../components/page_header';

export const Page: FC = () => {
const {
services: { share },
services: {
share,
notifications: { toasts },
},
} = useMlKibana();

const dataSourceContext = useDataSource();
Expand All @@ -48,7 +50,22 @@ export const Page: FC = () => {

const { selectedDataView, selectedSavedSearch } = dataSourceContext;

const isTimeBasedIndex = timeBasedIndexCheck(selectedDataView);
const isTimeBasedIndex: boolean = selectedDataView.isTimeBased();

useEffect(() => {
if (!isTimeBasedIndex) {
toasts.addWarning({
title: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationTitle', {
defaultMessage: 'The data view {dataViewIndexPattern} is not based on a time series',
values: { dataViewIndexPattern: selectedDataView.getIndexPattern() },
}),
text: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationDescription', {
defaultMessage: 'Anomaly detection only runs over time-based indices',
}),
});
}
}, [isTimeBasedIndex, selectedDataView, toasts]);

const hasGeoFields = useMemo(
() =>
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getFunctionDescription, isMetricDetector } from '../../get_function_des
import { useToastNotificationService } from '../../../services/toast_notification_service';
import { useMlResultsService } from '../../../services/results_service';
import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
import type { MlEntity } from '../../../../embeddables';

const plotByFunctionOptions = [
{
Expand Down Expand Up @@ -50,7 +51,7 @@ export const PlotByFunctionControls = ({
setFunctionDescription: (func: string) => void;
selectedDetectorIndex: number;
selectedJobId: string;
selectedEntities: Record<string, any>;
selectedEntities?: MlEntity;
entityControlsCount: number;
}) => {
const toastNotificationService = useToastNotificationService();
Expand All @@ -59,7 +60,7 @@ export const PlotByFunctionControls = ({
const getFunctionDescriptionToPlot = useCallback(
async (
_selectedDetectorIndex: number,
_selectedEntities: Record<string, any>,
_selectedEntities: MlEntity | undefined,
_selectedJobId: string,
_selectedJob: CombinedJob
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import type { FieldDefinition } from '../../../services/results_service/result_service_rx';
import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors';
import { PlotByFunctionControls } from '../plot_function_controls';
import type { MlEntity } from '../../../../embeddables';

function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] {
if (!Array.isArray(fieldValues)) {
Expand Down Expand Up @@ -76,7 +77,7 @@ interface SeriesControlsProps {
functionDescription?: string;
job?: CombinedJob | MlJob;
selectedDetectorIndex: number;
selectedEntities: Record<string, any>;
selectedEntities?: MlEntity;
selectedJobId: JobId;
setFunctionDescription: (func: string) => void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
import { mlJobService } from '../services/job_service';
import type { Entity } from './components/entity_control/entity_control';
import type { JobId, CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import type { MlEntity } from '../../embeddables';

/**
* Extracts entities from the detector configuration
*/
export function getControlsForDetector(
selectedDetectorIndex: number,
selectedEntities: Record<string, any>,
selectedEntities: MlEntity | undefined,
selectedJobId: JobId,
job?: CombinedJob
): Entity[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getControlsForDetector } from './get_controls_for_detector';
import { getCriteriaFields } from './get_criteria_fields';
import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors';
import type { MlEntity } from '../../embeddables';

export function isMetricDetector(selectedJob: CombinedJob, selectedDetectorIndex: number) {
const detectors = getViewableDetectors(selectedJob);
Expand All @@ -37,7 +38,7 @@ export const getFunctionDescription = async (
selectedJob,
}: {
selectedDetectorIndex: number;
selectedEntities: Record<string, any>;
selectedEntities: MlEntity | undefined;
selectedJobId: string;
selectedJob: CombinedJob;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component {
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
previousProps.selectedJob?.job_id !== this.props.selectedJob?.job_id ||
previousProps.selectedJobId !== this.props.selectedJobId ||
previousProps.functionDescription !== this.props.functionDescription
) {
Expand All @@ -727,6 +728,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component {
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
previousProps.selectedJobId !== this.props.selectedJobId ||
previousProps.selectedJob?.job_id !== this.props.selectedJob?.job_id ||
previousProps.functionDescription !== this.props.functionDescription;
this.loadSingleMetricData(fullRefresh);
}
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/ml/public/application/util/index_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { Job } from '../../../common/types/anomaly_detection_jobs';

import { useMlKibana } from '../contexts/kibana';

// TODO Consolidate with legacy code in `ml/public/application/util/index_utils.ts`.
export function indexServiceFactory(dataViewsService: DataViewsContract) {
/**
* Retrieves the data view ID from the given name.
Expand Down
27 changes: 0 additions & 27 deletions x-pack/plugins/ml/public/application/util/index_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
* 2.0.
*/

import { i18n } from '@kbn/i18n';
import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { Query, Filter } from '@kbn/es-query';
import type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { getToastNotifications } from './dependency_cache';

export interface DataViewAndSavedSearch {
savedSearch: SavedSearch | null;
Expand Down Expand Up @@ -48,31 +46,6 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearch) {
};
}

/**
* Returns true if the index passed in is time based
* an optional flag will trigger the display a notification at the top of the page
* warning that the index is not time based
*/
export function timeBasedIndexCheck(dataView: DataView, showNotification = false) {
if (!dataView.isTimeBased()) {
if (showNotification) {
const toastNotifications = getToastNotifications();
toastNotifications.addWarning({
title: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationTitle', {
defaultMessage: 'The data view {dataViewIndexPattern} is not based on a time series',
values: { dataViewIndexPattern: dataView.getIndexPattern() },
}),
text: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationDescription', {
defaultMessage: 'Anomaly detection only runs over time-based indices',
}),
});
}
return false;
} else {
return true;
}
}

/**
* Returns true if the index pattern contains a :
* which means it is cross-cluster
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/public/embeddables/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ export const ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE = 'ml_single_metric_vi

export type AnomalySwimLaneEmbeddableType = typeof ANOMALY_SWIMLANE_EMBEDDABLE_TYPE;
export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE;
export type AnomalySingleMetricViewerEmbeddableType =
typeof ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE;

export type MlEmbeddableTypes = AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType;
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface AppStateZoom {
export interface EmbeddableSingleMetricViewerContainerProps {
id: string;
embeddableContext: InstanceType<ISingleMetricViewerEmbeddable>;
embeddableInput: Observable<SingleMetricViewerEmbeddableInput>;
embeddableInput$: Observable<SingleMetricViewerEmbeddableInput>;
services: SingleMetricViewerEmbeddableServices;
refresh: Observable<void>;
onInputChange: (input: Partial<SingleMetricViewerEmbeddableInput>) => void;
Expand All @@ -50,10 +50,10 @@ export interface EmbeddableSingleMetricViewerContainerProps {

export const EmbeddableSingleMetricViewerContainer: FC<
EmbeddableSingleMetricViewerContainerProps
> = ({ id, embeddableContext, embeddableInput, services, refresh, onRenderComplete }) => {
> = ({ id, embeddableContext, embeddableInput$, services, refresh, onRenderComplete }) => {
useEmbeddableExecutionContext<SingleMetricViewerEmbeddableInput>(
services[0].executionContext,
embeddableInput,
embeddableInput$,
ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE,
id
);
Expand All @@ -66,7 +66,7 @@ export const EmbeddableSingleMetricViewerContainer: FC<

const { mlApiServices, mlJobService } = services[2];
const { data, bounds, lastRefresh } = useSingleMetricViewerInputResolver(
embeddableInput,
embeddableInput$,
refresh,
services[1].data.query.timefilter.timefilter,
onRenderComplete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { pick } from 'lodash';

import { Embeddable } from '@kbn/embeddable-plugin/public';
import { Embeddable, embeddableInputToSubject } from '@kbn/embeddable-plugin/public';
import { Subject, Subscription, type BehaviorSubject } from 'rxjs';

import type { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { Subject } from 'rxjs';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { IContainer } from '@kbn/embeddable-plugin/public';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
Expand All @@ -28,6 +28,7 @@ import type {
} from '..';
import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..';
import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback';
import type { MlEntity } from '..';

export const getDefaultSingleMetricViewerPanelTitle = (jobIds: JobId[]) =>
i18n.translate('xpack.ml.singleMetricViewerEmbeddable.title', {
Expand All @@ -45,12 +46,49 @@ export class SingleMetricViewerEmbeddable extends Embeddable<
private reload$ = new Subject<void>();
public readonly type: string = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE;

// API
public readonly functionDescription: BehaviorSubject<string | undefined>;
public readonly jobIds: BehaviorSubject<JobId[] | undefined>;
public readonly selectedDetectorIndex: BehaviorSubject<number | undefined>;
public readonly selectedEntities: BehaviorSubject<MlEntity | undefined>;

private apiSubscriptions = new Subscription();

constructor(
initialInput: SingleMetricViewerEmbeddableInput,
public services: [CoreStart, MlDependencies, SingleMetricViewerServices],
parent?: IContainer
) {
super(initialInput, {} as AnomalyChartsEmbeddableOutput, parent);

this.jobIds = embeddableInputToSubject<JobId[], SingleMetricViewerEmbeddableInput>(
this.apiSubscriptions,
this,
'jobIds'
);

this.functionDescription = embeddableInputToSubject<
string | undefined,
SingleMetricViewerEmbeddableInput
>(this.apiSubscriptions, this, 'functionDescription');

this.selectedDetectorIndex = embeddableInputToSubject<
number | undefined,
SingleMetricViewerEmbeddableInput
>(this.apiSubscriptions, this, 'selectedDetectorIndex');

this.selectedEntities = embeddableInputToSubject<
MlEntity | undefined,
SingleMetricViewerEmbeddableInput
>(this.apiSubscriptions, this, 'selectedEntities');
}

public updateUserInput(update: SingleMetricViewerEmbeddableInput) {
this.updateInput(update);
}

public reportsEmbeddableLoad() {
return true;
}

public onLoading() {
Expand All @@ -65,7 +103,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable<

public onRenderComplete() {
this.renderComplete.dispatchComplete();
this.updateOutput({ loading: false, error: undefined });
this.updateOutput({ loading: false, rendered: true, error: undefined });
}

public render(node: HTMLElement) {
Expand Down Expand Up @@ -102,7 +140,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable<
<EmbeddableSingleMetricViewerContainer
id={this.input.id}
embeddableContext={this}
embeddableInput={this.getInput$()}
embeddableInput$={this.getInput$()}
services={this.services}
refresh={this.reload$.asObservable()}
onInputChange={this.updateInput.bind(this)}
Expand All @@ -121,6 +159,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable<
}

public destroy() {
this.apiSubscriptions.unsubscribe();
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 { ISingleMetricViewerEmbeddable } from './single_metric_viewer_embeddable';

export class SingleMetricViewerEmbeddableFactory
implements EmbeddableFactoryDefinition<SingleMetricViewerEmbeddableInput>
Expand Down Expand Up @@ -58,11 +59,16 @@ export class SingleMetricViewerEmbeddableFactory
const { resolveEmbeddableSingleMetricViewerUserInput } = await import(
'./single_metric_viewer_setup_flyout'
);
return await resolveEmbeddableSingleMetricViewerUserInput(
const userInput = await resolveEmbeddableSingleMetricViewerUserInput(
coreStart,
pluginStart,
singleMetricServices
singleMetricServices.mlApiServices
);

return {
...userInput,
title: userInput.panelTitle,
};
} catch (e) {
return Promise.reject();
}
Expand Down Expand Up @@ -142,7 +148,10 @@ export class SingleMetricViewerEmbeddableFactory
];
}

public async create(initialInput: SingleMetricViewerEmbeddableInput, parent?: IContainer) {
public async create(
initialInput: SingleMetricViewerEmbeddableInput,
parent?: IContainer
): Promise<InstanceType<ISingleMetricViewerEmbeddable>> {
const services = await this.getServices();
const { SingleMetricViewerEmbeddable } = await import('./single_metric_viewer_embeddable');
return new SingleMetricViewerEmbeddable(initialInput, services, parent);
Expand Down
Loading

0 comments on commit 339e24f

Please sign in to comment.