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] Decouple edit-anomaly-swimlane UI action #179073

Merged
merged 9 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
import { Subject, Subscription, type BehaviorSubject } from 'rxjs';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { IContainer } from '@kbn/embeddable-plugin/public';
import { embeddableInputToSubject } from '@kbn/embeddable-plugin/public';
import { embeddableOutputToSubject } from '@kbn/embeddable-plugin/public';
import type { MlEntityField } from '@kbn/ml-anomaly-utils';
import { EmbeddableAnomalyChartsContainer } from './embeddable_anomaly_charts_container_lazy';
Expand Down Expand Up @@ -43,6 +44,7 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable<
public readonly type: string = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE;

// API
public readonly jobIds: BehaviorSubject<JobId[] | undefined>;
public entityFields: BehaviorSubject<MlEntityField[] | undefined>;

private apiSubscriptions = new Subscription();
Expand All @@ -54,6 +56,12 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable<
) {
super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent);

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

this.entityFields = embeddableOutputToSubject<MlEntityField[], AnomalyChartsEmbeddableOutput>(
this.apiSubscriptions,
this,
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/ml/public/embeddables/anomaly_charts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { MlEntityField } from '@kbn/ml-anomaly-utils';
import type { HasType, PublishingSubject } from '@kbn/presentation-publishing';
import type { JobId } from '../../shared';
import type { AnomalyExplorerChartsEmbeddableType } from '../constants';
import type { MlEmbeddableBaseApi } from '../types';

export interface AnomalyChartsFieldSelectionApi {
jobIds: PublishingSubject<JobId[]>;
entityFields: PublishingSubject<MlEntityField[] | undefined>;
}

export type AnomalyChartsEmbeddableApi = HasType<AnomalyExplorerChartsEmbeddableType> &
Copy link
Contributor

Choose a reason for hiding this comment

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

This type is really clean and straightforward. Nice work!

This is not necessary, but interfaces have slightly better editor time performance than types, so we're starting to recommend using the interface syntax whenever possible. This could be rewritten like:

export interface AnomalyChartsEmbeddableApi extends HasType<AnomalyExplorerChartsEmbeddableType>, MlEmbeddableBaseApi, AnomalyChartsFieldSelectionApi {}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 517e8a8

MlEmbeddableBaseApi &
AnomalyChartsFieldSelectionApi;
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import { Subject, Subscription, type BehaviorSubject } from 'rxjs';
import type {
AnomalySwimlaneEmbeddableInput,
AnomalySwimlaneEmbeddableOutput,
AnomalySwimlaneEmbeddableUserInput,
AnomalySwimlaneServices,
} from '..';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..';
import type { JobId } from '../../../common/types/anomaly_detection_jobs';
import type { MlDependencies } from '../../application/app';
import type { SwimlaneType } from '../../application/explorer/explorer_constants';
import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions';
import { AnomalyDetectionEmbeddable } from '../common/anomaly_detection_embeddable';
import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback';
Expand All @@ -43,9 +45,12 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
public readonly type: string = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE;

// API
public viewBy: BehaviorSubject<string | undefined>;
public perPage: BehaviorSubject<number | undefined>;
public fromPage: BehaviorSubject<number | undefined>;
public readonly jobIds: BehaviorSubject<JobId[] | undefined>;
public readonly viewBy: BehaviorSubject<string | undefined>;
public readonly swimlaneType: BehaviorSubject<SwimlaneType | undefined>;
public readonly perPage: BehaviorSubject<number | undefined>;
public readonly fromPage: BehaviorSubject<number | undefined>;
public readonly interval: BehaviorSubject<number | undefined>;

private apiSubscriptions = new Subscription();

Expand All @@ -56,12 +61,24 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
) {
super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent);

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

this.viewBy = embeddableInputToSubject<string, AnomalySwimlaneEmbeddableInput>(
this.apiSubscriptions,
this,
'viewBy'
);

this.swimlaneType = embeddableInputToSubject<SwimlaneType, AnomalySwimlaneEmbeddableInput>(
this.apiSubscriptions,
this,
'swimlaneType'
);

this.perPage = embeddableOutputToSubject<number, AnomalySwimlaneEmbeddableOutput>(
this.apiSubscriptions,
this,
Expand All @@ -73,6 +90,16 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
this,
'fromPage'
);

this.interval = embeddableOutputToSubject<number, AnomalySwimlaneEmbeddableOutput>(
this.apiSubscriptions,
this,
'interval'
);
}

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

public reportsEmbeddableLoad() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ export class AnomalySwimlaneEmbeddableFactory

try {
const { resolveAnomalySwimlaneUserInput } = await import('./anomaly_swimlane_setup_flyout');
return await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews);
const userInput = await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews);

return {
...userInput,
title: userInput.panelTitle,
};
} catch (e) {
return Promise.reject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { SwimlaneType } from '../../application/explorer/explorer_constants';
import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
import type { AnomalySwimlaneEmbeddableInput } from '..';
import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableUserInput } from '..';

interface ExplicitInput {
panelTitle: string;
swimlaneType: SwimlaneType;
viewBy?: string;
}
export type ExplicitInput = Omit<AnomalySwimlaneEmbeddableUserInput, 'jobIds'>;

export interface AnomalySwimlaneInitializerProps {
defaultTitle: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants
import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
import { getDefaultSwimlanePanelTitle } from './anomaly_swimlane_embeddable';
import { HttpService } from '../../application/services/http_service';
import type { AnomalySwimlaneEmbeddableInput } from '..';
import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableUserInput } from '..';
import { resolveJobSelection } from '../common/resolve_job_selection';
import { mlApiServicesProvider } from '../../application/services/ml_api_service';

export async function resolveAnomalySwimlaneUserInput(
coreStart: CoreStart,
dataViews: DataViewsContract,
input?: AnomalySwimlaneEmbeddableInput
): Promise<Partial<AnomalySwimlaneEmbeddableInput>> {
input?: Partial<AnomalySwimlaneEmbeddableInput>
): Promise<AnomalySwimlaneEmbeddableUserInput> {
const { http, overlays, theme, i18n } = coreStart;

const { getJobs } = mlApiServicesProvider(new HttpService(http));
Expand All @@ -44,7 +44,6 @@ export async function resolveAnomalySwimlaneUserInput(
modalSession.close();
resolve({
jobIds,
title: explicitInput.panelTitle,
...explicitInput,
});
}}
Expand Down
31 changes: 31 additions & 0 deletions x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 {
HasType,
PublishesWritablePanelTitle,
PublishingSubject,
} from '@kbn/presentation-publishing';
import type { SwimlaneType } from '../../application/explorer/explorer_constants';
import type { JobId } from '../../shared';
import type { AnomalySwimLaneEmbeddableType } from '../constants';
import type { AnomalySwimlaneEmbeddableUserInput, MlEmbeddableBaseApi } from '../types';

export interface AnomalySwimLaneComponentApi {
jobIds: PublishingSubject<JobId[]>;
swimlaneType: PublishingSubject<SwimlaneType>;
viewBy: PublishingSubject<string>;
perPage: PublishingSubject<number>;
fromPage: PublishingSubject<number>;
interval: PublishingSubject<number | undefined>;
updateUserInput: (input: AnomalySwimlaneEmbeddableUserInput) => void;
}

export type AnomalySwimLaneEmbeddableApi = HasType<AnomalySwimLaneEmbeddableType> &
PublishesWritablePanelTitle &
MlEmbeddableBaseApi &
AnomalySwimLaneComponentApi;
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
* 2.0.
*/

import { type DataView } from '@kbn/data-views-plugin/common';
import { type DataViewsContract } from '@kbn/data-views-plugin/public';
import {
Embeddable,
type EmbeddableInput,
type EmbeddableOutput,
type IContainer,
} from '@kbn/embeddable-plugin/public';
import { type DataView } from '@kbn/data-views-plugin/common';
import { type DataViewsContract } from '@kbn/data-views-plugin/public';
import type { BehaviorSubject } from 'rxjs';
import { firstValueFrom } from 'rxjs';
import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import type { JobId } from '../../shared';
Expand All @@ -28,7 +29,8 @@ export abstract class AnomalyDetectionEmbeddable<
// Need to defer embeddable load in order to resolve data views
deferEmbeddableLoad = true;

public jobIds: JobId[] = [];
// API
public abstract jobIds: BehaviorSubject<JobId[] | undefined>;

protected constructor(
initialInput: Input,
Expand All @@ -46,8 +48,6 @@ export abstract class AnomalyDetectionEmbeddable<
protected async initializeOutput(initialInput: CommonInput) {
const { jobIds } = initialInput;

this.jobIds = jobIds;

try {
const jobs = await firstValueFrom(this.anomalyDetectorService.getJobs$(jobIds));

Expand Down
30 changes: 28 additions & 2 deletions x-pack/plugins/ml/public/embeddables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import type { MlEntityField } from '@kbn/ml-anomaly-utils';
import type {
EmbeddableApiContext,
HasParentApi,
HasType,
PublishesPanelTitle,
PublishesUnifiedSearch,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import type { JobId } from '../../common/types/anomaly_detection_jobs';
import type { MlDependencies } from '../application/app';
import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities';
Expand All @@ -30,6 +38,21 @@ import type {
MlEmbeddableTypes,
} from './constants';

export type MlEmbeddableBaseApi = Partial<
HasParentApi<PublishesUnifiedSearch> &
PublishesViewMode &
PublishesUnifiedSearch &
PublishesPanelTitle
>;

/** Manual input by the user */
export interface AnomalySwimlaneEmbeddableUserInput {
jobIds: JobId[];
panelTitle: string;
swimlaneType: SwimlaneType;
viewBy?: string;
}

export interface AnomalySwimlaneEmbeddableCustomInput {
jobIds: JobId[];
swimlaneType: SwimlaneType;
Expand Down Expand Up @@ -66,8 +89,11 @@ export interface AnomalySwimlaneEmbeddableCustomOutput {
export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput &
AnomalySwimlaneEmbeddableCustomOutput;

export interface EditSwimlanePanelContext {
embeddable: IEmbeddable<AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput>;
export type EditSwimLaneActionApi = HasType<AnomalySwimLaneEmbeddableType> &
Partial<HasParentApi<PublishesUnifiedSearch>>;

export interface EditSwimlanePanelContext extends EmbeddableApiContext {
embeddable: EditSwimLaneActionApi;
}

export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext {
Expand Down
30 changes: 20 additions & 10 deletions x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,32 @@
* 2.0.
*/

import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
import moment from 'moment';
import { firstValueFrom } from 'rxjs';
import type { AppStateSelectedCells } from '../application/explorer/explorer_utils';
import type { AnomalySwimLaneEmbeddableApi } from '../embeddables/anomaly_swimlane/types';
import type { MlCoreSetup } from '../plugin';
import type { SwimLaneDrilldownContext } from '../embeddables';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../embeddables';
import { isSwimLaneEmbeddableContext } from './open_in_anomaly_explorer_action';

export const APPLY_TIME_RANGE_SELECTION_ACTION = 'applyTimeRangeSelectionAction';

const supportedApps = [DASHBOARD_APP_ID];

export interface ApplyTimeRangeSelectionActionContext extends EmbeddableApiContext {
embeddable: AnomalySwimLaneEmbeddableApi;
/**
* Optional data provided by swim lane selection
*/
data?: AppStateSelectedCells;
}

export function createApplyTimeRangeSelectionAction(
getStartServices: MlCoreSetup['getStartServices']
): UiActionsActionDefinition<SwimLaneDrilldownContext> {
): UiActionsActionDefinition<ApplyTimeRangeSelectionActionContext> {
return {
id: 'apply-time-range-selection',
type: APPLY_TIME_RANGE_SELECTION_ACTION,
Expand All @@ -37,9 +47,9 @@ export function createApplyTimeRangeSelectionAction(
}
const [, pluginStart] = await getStartServices();
const timefilter = pluginStart.data.query.timefilter.timefilter;
const { interval } = embeddable.getOutput();
const { interval } = embeddable;

if (!interval) {
if (!interval.getValue()) {
throw new Error('Interval is required to set a time range');
}

Expand All @@ -53,12 +63,12 @@ export function createApplyTimeRangeSelectionAction(
mode: 'absolute',
});
},
async isCompatible({ embeddable, data }) {
async isCompatible(context) {
const [{ application }] = await getStartServices();
const appId = await firstValueFrom(application.currentAppId$);
return (
embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE &&
data !== undefined &&
isSwimLaneEmbeddableContext(context) &&
context.data !== undefined &&
supportedApps.includes(appId!)
);
},
Expand Down
Loading