Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ML] Single Metric Viewer embeddable: add open and edit panel actions #179364

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]);

Comment on lines +53 to +68
Copy link
Contributor

@darnautov darnautov Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can be simplified even further, preventing access to the isTImeBased method on each render!

Suggested change
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]);
useEffect(function warnNonTimeSeriesData() {
if (!selectedDataView.isTimeBased()) {
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',
}),
});
}
}, [selectedDataView, toasts]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a reliance on the isTimeBasedIndex variable a few lines down - I considered wrapping in a useEffect but I think the addition of the function would negate any small optimization we'd get from that, in my opinion. Happy to discuss further if you feel strongly. 😄

const hasGeoFields = useMemo(
() =>
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const PlotByFunctionControls = ({
setFunctionDescription: (func: string) => void;
selectedDetectorIndex: number;
selectedJobId: string;
selectedEntities: Record<string, any>;
selectedEntities?: Record<string, any>;
entityControlsCount: number;
}) => {
const toastNotificationService = useToastNotificationService();
Expand All @@ -59,7 +59,7 @@ export const PlotByFunctionControls = ({
const getFunctionDescriptionToPlot = useCallback(
async (
_selectedDetectorIndex: number,
_selectedEntities: Record<string, any>,
_selectedEntities: Record<string, any> | undefined,
_selectedJobId: string,
_selectedJob: CombinedJob
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ interface SeriesControlsProps {
functionDescription?: string;
job?: CombinedJob | MlJob;
selectedDetectorIndex: number;
selectedEntities: Record<string, any>;
selectedEntities?: Record<string, any>;
selectedJobId: JobId;
setFunctionDescription: (func: string) => void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { JobId, CombinedJob } from '../../../common/types/anomaly_detection
*/
export function getControlsForDetector(
selectedDetectorIndex: number,
selectedEntities: Record<string, any>,
selectedEntities: Record<string, any> | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please use the MlEntityField type instead of any

Copy link
Contributor Author

@alvarezmelissa87 alvarezmelissa87 Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back to double check and the original Record<string, any> type is correct - went back to ensure the types are correct. All types should now be correct. Updated in c39b883

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any is correct. If you don't know the incoming type - it should be unknown and covered with a type guard. Otherwise you should be aware of the expected type.

selectedJobId: JobId,
job?: CombinedJob
): Entity[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const getFunctionDescription = async (
selectedJob,
}: {
selectedDetectorIndex: number;
selectedEntities: Record<string, any>;
selectedEntities: Record<string, any> | 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 @@ -67,7 +67,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,12 @@ 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 { MlEntityField } from '@kbn/ml-anomaly-utils';

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 Down Expand Up @@ -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<MlEntityField[] | 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<
MlEntityField[] | 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