mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.x] [ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987) (#193568)
# Backport This will backport the following commits from `main` to `8.x`: - [[ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987)](https://github.com/elastic/kibana/pull/192987) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Robert Jaszczurek","email":"92210485+rbrtj@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-09-20T12:06:29Z","message":"[ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987)\n\n# Summary\r\n\r\nFix for [#192679](https://github.com/elastic/kibana/issues/192679)\r\nHiding the top influencers panel when there are no influencers for the\r\nselected job.\r\nAdded a functional test to ensure the panel is hidden.\r\nExpanded a few types to improve type safety.","sha":"23b2595be39401214a1ef9e39b684f917020b9ad","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug",":ml","release_note:skip","v9.0.0","Team:ML","backport:prev-minor","v8.16.0"],"title":"[ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers","number":192987,"url":"https://github.com/elastic/kibana/pull/192987","mergeCommit":{"message":"[ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987)\n\n# Summary\r\n\r\nFix for [#192679](https://github.com/elastic/kibana/issues/192679)\r\nHiding the top influencers panel when there are no influencers for the\r\nselected job.\r\nAdded a functional test to ensure the panel is hidden.\r\nExpanded a few types to improve type safety.","sha":"23b2595be39401214a1ef9e39b684f917020b9ad"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192987","number":192987,"mergeCommit":{"message":"[ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987)\n\n# Summary\r\n\r\nFix for [#192679](https://github.com/elastic/kibana/issues/192679)\r\nHiding the top influencers panel when there are no influencers for the\r\nselected job.\r\nAdded a functional test to ensure the panel is hidden.\r\nExpanded a few types to improve type safety.","sha":"23b2595be39401214a1ef9e39b684f917020b9ad"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com>
This commit is contained in:
parent
5844ba0e47
commit
5f2de6d6d5
8 changed files with 588 additions and 493 deletions
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from } from 'rxjs';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
|
@ -12,13 +13,14 @@ import type { MlFieldFormatService } from '../../services/field_format_service';
|
|||
import type { MlJobService } from '../../services/job_service';
|
||||
|
||||
import { EXPLORER_ACTION } from '../explorer_constants';
|
||||
import { createJobs } from '../explorer_utils';
|
||||
import { createJobs, getInfluencers } from '../explorer_utils';
|
||||
import type { ExplorerActions } from '../explorer_dashboard_service';
|
||||
|
||||
export function jobSelectionActionCreator(
|
||||
mlJobService: MlJobService,
|
||||
mlFieldFormatService: MlFieldFormatService,
|
||||
selectedJobIds: string[]
|
||||
) {
|
||||
): Observable<ExplorerActions | null> {
|
||||
return from(mlFieldFormatService.populateFormats(selectedJobIds)).pipe(
|
||||
map((resp) => {
|
||||
if (resp.error) {
|
||||
|
@ -31,12 +33,14 @@ export function jobSelectionActionCreator(
|
|||
});
|
||||
|
||||
const selectedJobs = jobs.filter((job) => job.selected);
|
||||
const noInfluencersConfigured = getInfluencers(mlJobService, selectedJobs).length === 0;
|
||||
|
||||
return {
|
||||
type: EXPLORER_ACTION.JOB_SELECTION_CHANGE,
|
||||
payload: {
|
||||
loading: false,
|
||||
selectedJobs,
|
||||
noInfluencersConfigured,
|
||||
},
|
||||
};
|
||||
})
|
||||
|
|
|
@ -24,7 +24,7 @@ export const EXPLORER_ACTION = {
|
|||
JOB_SELECTION_CHANGE: 'jobSelectionChange',
|
||||
SET_CHARTS_DATA_LOADING: 'setChartsDataLoading',
|
||||
SET_EXPLORER_DATA: 'setExplorerData',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const FILTER_ACTION = {
|
||||
ADD: '+',
|
||||
|
|
|
@ -21,21 +21,38 @@ import type { ExplorerState } from './reducers';
|
|||
import { explorerReducer, getExplorerDefaultState } from './reducers';
|
||||
import type { MlFieldFormatService } from '../services/field_format_service';
|
||||
import type { MlJobService } from '../services/job_service';
|
||||
import type { ExplorerJob } from './explorer_utils';
|
||||
|
||||
type ExplorerAction = Action | Observable<ActionPayload>;
|
||||
export const explorerAction$ = new Subject<ExplorerAction>();
|
||||
type ExplorerAction = (typeof EXPLORER_ACTION)[keyof typeof EXPLORER_ACTION];
|
||||
|
||||
export type ActionPayload = any;
|
||||
|
||||
export interface Action {
|
||||
type: string;
|
||||
payload?: ActionPayload;
|
||||
export interface ExplorerActionPayloads {
|
||||
[EXPLORER_ACTION.SET_EXPLORER_DATA]: DeepPartial<ExplorerState>;
|
||||
[EXPLORER_ACTION.JOB_SELECTION_CHANGE]: {
|
||||
loading: boolean;
|
||||
selectedJobs: ExplorerJob[];
|
||||
noInfluencersConfigured: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type ExplorerActions = {
|
||||
[K in ExplorerAction]: K extends keyof ExplorerActionPayloads
|
||||
? {
|
||||
type: K;
|
||||
payload: ExplorerActionPayloads[K];
|
||||
}
|
||||
: {
|
||||
type: K;
|
||||
};
|
||||
}[ExplorerAction];
|
||||
|
||||
type ExplorerActionMaybeObservable = ExplorerActions | Observable<ExplorerActions | null>;
|
||||
|
||||
export const explorerAction$ = new Subject<ExplorerActionMaybeObservable>();
|
||||
|
||||
const explorerFilteredAction$ = explorerAction$.pipe(
|
||||
// consider observables as side-effects
|
||||
flatMap((action: ExplorerAction) =>
|
||||
isObservable(action) ? action : (from([action]) as Observable<ExplorerAction>)
|
||||
flatMap((action: ExplorerActionMaybeObservable) =>
|
||||
isObservable(action) ? action : (from([action]) as Observable<ExplorerActionMaybeObservable>)
|
||||
),
|
||||
distinctUntilChanged(isEqual)
|
||||
);
|
||||
|
@ -47,11 +64,6 @@ const explorerState$: Observable<ExplorerState> = explorerFilteredAction$.pipe(
|
|||
shareReplay(1)
|
||||
);
|
||||
|
||||
const setExplorerDataActionCreator = (payload: DeepPartial<ExplorerState>) => ({
|
||||
type: EXPLORER_ACTION.SET_EXPLORER_DATA,
|
||||
payload,
|
||||
});
|
||||
|
||||
// Export observable state and action dispatchers as service
|
||||
export const explorerServiceFactory = (
|
||||
mlJobService: MlJobService,
|
||||
|
@ -62,7 +74,9 @@ export const explorerServiceFactory = (
|
|||
explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA });
|
||||
},
|
||||
clearInfluencerFilterSettings: () => {
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS });
|
||||
explorerAction$.next({
|
||||
type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS,
|
||||
});
|
||||
},
|
||||
clearJobs: () => {
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS });
|
||||
|
@ -73,7 +87,7 @@ export const explorerServiceFactory = (
|
|||
);
|
||||
},
|
||||
setExplorerData: (payload: DeepPartial<ExplorerState>) => {
|
||||
explorerAction$.next(setExplorerDataActionCreator(payload));
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_EXPLORER_DATA, payload });
|
||||
},
|
||||
setChartsDataLoading: () => {
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING });
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
*/
|
||||
|
||||
import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns';
|
||||
import type { ExplorerJob } from '../../explorer_utils';
|
||||
|
||||
// Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider
|
||||
// Field objects required fields: name, type, aggregatable, searchable
|
||||
export function getIndexPattern(influencers: string[]) {
|
||||
export function getIndexPattern(influencers: ExplorerJob[]) {
|
||||
return {
|
||||
title: ML_RESULTS_INDEX_PATTERN,
|
||||
fields: influencers.map((influencer) => ({
|
||||
name: influencer,
|
||||
name: influencer.id,
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ActionPayload } from '../../explorer_dashboard_service';
|
||||
import type { EXPLORER_ACTION } from '../../explorer_constants';
|
||||
import type { ExplorerActionPayloads } from '../../explorer_dashboard_service';
|
||||
|
||||
import { getIndexPattern } from './get_index_pattern';
|
||||
import type { ExplorerState } from './state';
|
||||
|
||||
export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload): ExplorerState => {
|
||||
export const jobSelectionChange = (
|
||||
state: ExplorerState,
|
||||
payload: ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE]
|
||||
): ExplorerState => {
|
||||
const { selectedJobs, noInfluencersConfigured } = payload;
|
||||
const stateUpdate: ExplorerState = {
|
||||
...state,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { getDefaultChartsData } from '../../explorer_charts/explorer_charts_container_service';
|
||||
import { EXPLORER_ACTION } from '../../explorer_constants';
|
||||
import type { Action } from '../../explorer_dashboard_service';
|
||||
import type { ExplorerActionPayloads, ExplorerActions } from '../../explorer_dashboard_service';
|
||||
import { getClearedSelectedAnomaliesState } from '../../explorer_utils';
|
||||
|
||||
import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings';
|
||||
|
@ -16,8 +16,12 @@ import type { ExplorerState } from './state';
|
|||
import { getExplorerDefaultState } from './state';
|
||||
import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder';
|
||||
|
||||
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
|
||||
const { type, payload } = nextAction;
|
||||
export const explorerReducer = (
|
||||
state: ExplorerState,
|
||||
nextAction: ExplorerActions
|
||||
): ExplorerState => {
|
||||
const { type } = nextAction;
|
||||
const payload = 'payload' in nextAction ? nextAction.payload : {};
|
||||
|
||||
let nextState: ExplorerState;
|
||||
|
||||
|
@ -40,7 +44,10 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
|||
break;
|
||||
|
||||
case EXPLORER_ACTION.JOB_SELECTION_CHANGE:
|
||||
nextState = jobSelectionChange(state, payload);
|
||||
nextState = jobSelectionChange(
|
||||
state,
|
||||
payload as ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE]
|
||||
);
|
||||
break;
|
||||
|
||||
case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING:
|
||||
|
@ -52,7 +59,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
|||
break;
|
||||
|
||||
case EXPLORER_ACTION.SET_EXPLORER_DATA:
|
||||
nextState = { ...state, ...payload };
|
||||
nextState = { ...state, ...(payload as Partial<ExplorerState>) };
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,6 +30,10 @@ export function MachineLearningAnomalyExplorerProvider(
|
|||
await testSubjects.existOrFail('mlAnomalyExplorerInfluencerList');
|
||||
},
|
||||
|
||||
async assertInfluencerListDoesNotExist() {
|
||||
await testSubjects.missingOrFail('mlAnomalyExplorerInfluencerList');
|
||||
},
|
||||
|
||||
async assertInfluencerFieldExists(influencerField: string) {
|
||||
await testSubjects.existOrFail(`mlInfluencerFieldName ${influencerField}`);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue