[8.18] [ML] AIOps: Change Point Detection in Dashboards embeddable fix (#217178) (#217316)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[ML] AIOps: Change Point Detection in Dashboards embeddable fix
(#217178)](https://github.com/elastic/kibana/pull/217178)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Robert
Jaszczurek","email":"92210485+rbrtj@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-07T10:06:30Z","message":"[ML]
AIOps: Change Point Detection in Dashboards embeddable fix
(#217178)\n\nIt fixes an issue where adding the `Change Point Detection`
embeddable\ndidn't work properly.\nThe bug was introduced in
https://github.com/elastic/kibana/pull/197943\nThe main cause was the
use of `<ChangePointDetectionContextProvider>`\nwhich calls
`timefilter.getActiveBounds()`. However, for\n`getActiveBounds` to work,
`this.isTimeRangeSelectorEnabled()` must\nreturn true. By default, this
is not the case within dashboards.\nHowever, we do not actually need the
`ChangePointDetectionContext`\ninside the embeddable, so this PR removes
its usage.\nA functional test has been added to cover adding the Change
Point\nembeddable from the dashboards app. It's a very simple test that
does\nnot verify the embeddable's functionality, but it could be
improved in
a\nfollow-up.\n\n![image](https://github.com/user-attachments/assets/52b7f28b-87a0-423e-a923-d3e02300bf71)\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d35988152d9e95e26908046ea3540d34e47cf92f","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix",":ml","Feature:ML/AIOps","backport:version","v9.1.0","v8.19.0","v8.18.1","v9.0.1","v8.17.5"],"title":"[ML]
AIOps: Change Point Detection in Dashboards embeddable
fix","number":217178,"url":"https://github.com/elastic/kibana/pull/217178","mergeCommit":{"message":"[ML]
AIOps: Change Point Detection in Dashboards embeddable fix
(#217178)\n\nIt fixes an issue where adding the `Change Point Detection`
embeddable\ndidn't work properly.\nThe bug was introduced in
https://github.com/elastic/kibana/pull/197943\nThe main cause was the
use of `<ChangePointDetectionContextProvider>`\nwhich calls
`timefilter.getActiveBounds()`. However, for\n`getActiveBounds` to work,
`this.isTimeRangeSelectorEnabled()` must\nreturn true. By default, this
is not the case within dashboards.\nHowever, we do not actually need the
`ChangePointDetectionContext`\ninside the embeddable, so this PR removes
its usage.\nA functional test has been added to cover adding the Change
Point\nembeddable from the dashboards app. It's a very simple test that
does\nnot verify the embeddable's functionality, but it could be
improved in
a\nfollow-up.\n\n![image](https://github.com/user-attachments/assets/52b7f28b-87a0-423e-a923-d3e02300bf71)\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d35988152d9e95e26908046ea3540d34e47cf92f"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","8.18","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217178","number":217178,"mergeCommit":{"message":"[ML]
AIOps: Change Point Detection in Dashboards embeddable fix
(#217178)\n\nIt fixes an issue where adding the `Change Point Detection`
embeddable\ndidn't work properly.\nThe bug was introduced in
https://github.com/elastic/kibana/pull/197943\nThe main cause was the
use of `<ChangePointDetectionContextProvider>`\nwhich calls
`timefilter.getActiveBounds()`. However, for\n`getActiveBounds` to work,
`this.isTimeRangeSelectorEnabled()` must\nreturn true. By default, this
is not the case within dashboards.\nHowever, we do not actually need the
`ChangePointDetectionContext`\ninside the embeddable, so this PR removes
its usage.\nA functional test has been added to cover adding the Change
Point\nembeddable from the dashboards app. It's a very simple test that
does\nnot verify the embeddable's functionality, but it could be
improved in
a\nfollow-up.\n\n![image](https://github.com/user-attachments/assets/52b7f28b-87a0-423e-a923-d3e02300bf71)\n\n---------\n\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d35988152d9e95e26908046ea3540d34e47cf92f"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.5","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/217315","number":217315,"state":"OPEN"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Robert Jaszczurek 2025-04-07 16:07:33 +02:00 committed by GitHub
parent 0e90f2d2f2
commit a9a1667338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 50 deletions

View file

@ -20,21 +20,16 @@ import {
} from '@elastic/eui';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { FieldStatsFlyoutProvider } from '@kbn/ml-field-stats-flyout';
import { useTimefilter } from '@kbn/ml-date-picker';
import { pick } from 'lodash';
import type { FC } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import {
ChangePointDetectionContextProvider,
ChangePointDetectionControlsContextProvider,
useChangePointDetectionContext,
useChangePointDetectionControlsContext,
} from '../../components/change_point_detection/change_point_detection_context';
import { DEFAULT_AGG_FUNCTION } from '../../components/change_point_detection/constants';
@ -45,7 +40,7 @@ import { PartitionsSelector } from '../../components/change_point_detection/part
import { SplitFieldSelector } from '../../components/change_point_detection/split_field_selector';
import { ViewTypeSelector } from '../../components/change_point_detection/view_type_selector';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useDataSource, DataSourceContextProvider } from '../../hooks/use_data_source';
import { DataSourceContextProvider } from '../../hooks/use_data_source';
import { FilterQueryContextProvider } from '../../hooks/use_filters_query';
import { DEFAULT_SERIES } from './const';
import type { ChangePointEmbeddableRuntimeState } from './types';
@ -134,7 +129,7 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm>
<EuiForm data-test-subj="aiopsChangePointDetectionControls">
<ViewTypeSelector value={viewType} onChange={setViewType} />
<EuiFormRow
fullWidth
@ -156,21 +151,20 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
onChange={(newId) => {
setDataViewId(newId ?? '');
}}
data-test-subj="aiopsChangePointChartEmbeddableDataViewSelector"
/>
</EuiFormRow>
<EuiHorizontalRule margin={'s'} />
<DataSourceContextProvider dataViews={dataViews} dataViewId={dataViewId}>
<DatePickerContextProvider {...datePickerDeps}>
<FilterQueryContextProvider>
<ChangePointDetectionContextProvider>
<ChangePointDetectionControlsContextProvider>
<FormControls
formInput={formInput}
onChange={setFormInput}
onValidationChange={setIsFormValid}
/>
</ChangePointDetectionControlsContextProvider>
</ChangePointDetectionContextProvider>
<ChangePointDetectionControlsContextProvider>
<FormControls
formInput={formInput}
onChange={setFormInput}
onValidationChange={setIsFormValid}
/>
</ChangePointDetectionControlsContextProvider>
</FilterQueryContextProvider>
</DatePickerContextProvider>
</DataSourceContextProvider>
@ -219,12 +213,7 @@ export const FormControls: FC<{
onChange: (update: FormControlsProps) => void;
onValidationChange: (isValid: boolean) => void;
}> = ({ formInput, onChange, onValidationChange }) => {
const { charts, data, fieldFormats, theme, uiSettings } = useAiopsAppContext();
const { dataView } = useDataSource();
const { combinedQuery } = useChangePointDetectionContext();
const { metricFieldOptions, splitFieldsOptions } = useChangePointDetectionControlsContext();
const timefilter = useTimefilter();
const timefilterActiveBounds = timefilter.getActiveBounds();
const prevMetricFieldOptions = usePrevious(metricFieldOptions);
@ -273,33 +262,10 @@ export const FormControls: FC<{
[formInput, onChange]
);
const fieldStatsServices: FieldStatsServices = useMemo(() => {
return {
uiSettings,
dataViews: data.dataViews,
data,
fieldFormats,
charts,
};
}, [uiSettings, data, fieldFormats, charts]);
if (!isPopulatedObject(formInput)) return null;
return (
<FieldStatsFlyoutProvider
fieldStatsServices={fieldStatsServices}
dataView={dataView}
dslQuery={combinedQuery}
timeRangeMs={
timefilterActiveBounds
? {
from: timefilterActiveBounds.min!.valueOf(),
to: timefilterActiveBounds.max!.valueOf(),
}
: undefined
}
theme={theme}
>
<>
<EuiFormRow
fullWidth
label={
@ -340,6 +306,6 @@ export const FormControls: FC<{
onChange={(v) => updateCallback({ maxSeriesToPlot: v })}
onValidationChange={(result) => onValidationChange(result === null)}
/>
</FieldStatsFlyoutProvider>
</>
);
};

View file

@ -5,8 +5,11 @@
* 2.0.
*/
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '@kbn/aiops-change-point-detection/constants';
import { FtrProviderContext } from '../../ftr_provider_context';
const dashboardTitle = 'Change point detection';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const elasticChart = getService('elasticChart');
const esArchiver = getService('esArchiver');
@ -15,6 +18,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
// aiops lives in the ML UI so we need some related services.
const ml = getService('ml');
const PageObjects = getPageObjects(['dashboard']);
describe('change point detection in dashboard', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce');
@ -24,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
after(async () => {
await ml.testResources.deleteDashboardByTitle(dashboardTitle);
await ml.testResources.deleteDataViewByTitle('ft_ecommerce');
});
@ -44,5 +50,29 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
maxSeries: 1,
});
});
it('attaches change point charts to a dashboard from the dashboard app', async () => {
await PageObjects.dashboard.navigateToApp();
await PageObjects.dashboard.clickNewDashboard();
await aiops.dashboardEmbeddables.assertDashboardIsEmpty();
await aiops.dashboardEmbeddables.openEmbeddableInitializer(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE
);
await aiops.dashboardEmbeddables.assertInitializerConfirmButtonEnabled(
'aiopsChangePointChartsInitializerConfirmButton',
false
);
await aiops.dashboardEmbeddables.assertChangePointChartEmbeddableDataViewSelectorExists();
await aiops.dashboardEmbeddables.selectChangePointChartEmbeddableDataView('ft_ecommerce');
await aiops.dashboardEmbeddables.assertInitializerConfirmButtonEnabled(
'aiopsChangePointChartsInitializerConfirmButton'
);
await aiops.dashboardEmbeddables.submitChangePointInitForm();
await aiops.dashboardEmbeddables.assertChangePointPanelExists();
await PageObjects.dashboard.saveDashboard(dashboardTitle);
});
});
}

View file

@ -9,6 +9,8 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
type AiopsEmbeddableType = 'aiopsLogRateAnalysisEmbeddable' | 'aiopsChangePointChart';
export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderContext) {
const comboBox = getService('comboBox');
const retry = getService('retry');
@ -33,10 +35,29 @@ export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderCon
});
},
async assertInitializerConfirmButtonEnabled(subj: string) {
async assertChangePointChartInitializerExists(expectExists = true) {
await retry.tryForTime(10 * 1000, async () => {
if (expectExists) {
await testSubjects.existOrFail('aiopsChangePointChartEmbeddableInitializer', {
timeout: 1000,
});
} else {
await testSubjects.missingOrFail('aiopsChangePointChartEmbeddableInitializer', {
timeout: 1000,
});
}
});
},
async assertInitializerConfirmButtonEnabled(subj: string, expectEnabled = true) {
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail(subj);
await testSubjects.isEnabled(subj);
const isEnabled = await testSubjects.isEnabled(subj);
if (expectEnabled) {
expect(isEnabled).to.be(true);
} else {
expect(isEnabled).to.be(false);
}
});
},
@ -68,9 +89,18 @@ export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderCon
});
},
async openEmbeddableInitializer(mlEmbeddableType: 'aiopsLogRateAnalysisEmbeddable') {
async submitChangePointInitForm() {
const subj = 'aiopsChangePointChartsInitializerConfirmButton';
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.clickWhenNotDisabledWithoutRetry(subj);
await this.assertChangePointChartInitializerExists(false);
});
},
async openEmbeddableInitializer(mlEmbeddableType: AiopsEmbeddableType) {
const name = {
aiopsLogRateAnalysisEmbeddable: 'Log rate analysis',
aiopsChangePointChart: 'Change point detection',
};
await retry.tryForTime(60 * 1000, async () => {
await dashboardAddPanel.clickEditorMenuButton();
@ -79,16 +109,34 @@ export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderCon
await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('logs-aiops');
await dashboardAddPanel.clickAddNewPanelFromUIActionLink(name[mlEmbeddableType]);
await testSubjects.existOrFail('aiopsLogRateAnalysisControls', { timeout: 2000 });
await this.assertEmbeddableControlsExist(mlEmbeddableType);
});
},
async assertEmbeddableControlsExist(mlEmbeddableType: AiopsEmbeddableType) {
const controlSelectors = {
aiopsLogRateAnalysisEmbeddable: 'aiopsLogRateAnalysisControls',
aiopsChangePointChart: 'aiopsChangePointDetectionControls',
};
await testSubjects.existOrFail(controlSelectors[mlEmbeddableType], { timeout: 2000 });
},
async assertChangePointPanelExists() {
await testSubjects.existOrFail('aiopsEmbeddableChangePointChart');
},
async assertLogRateAnalysisEmbeddableDataViewSelectorExists() {
await testSubjects.existOrFail(
'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput'
);
},
async assertChangePointChartEmbeddableDataViewSelectorExists() {
await testSubjects.existOrFail(
'aiopsChangePointChartEmbeddableDataViewSelector > comboBoxInput'
);
},
async assertLogRateAnalysisEmbeddableDataViewSelection(dataViewValue: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput'
@ -99,6 +147,16 @@ export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderCon
);
},
async assertChangePointChartEmbeddableDataViewSelection(dataViewValue: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'aiopsChangePointChartEmbeddableDataViewSelector > comboBoxInput'
);
expect(comboBoxSelectedOptions).to.eql(
[dataViewValue],
`Expected data view selection to be '${dataViewValue}' (got '${comboBoxSelectedOptions}')`
);
},
async selectLogRateAnalysisEmbeddableDataView(dataViewValue: string) {
await comboBox.set(
'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput',
@ -106,5 +164,13 @@ export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderCon
);
await this.assertLogRateAnalysisEmbeddableDataViewSelection(dataViewValue);
},
async selectChangePointChartEmbeddableDataView(dataViewValue: string) {
await comboBox.set(
'aiopsChangePointChartEmbeddableDataViewSelector > comboBoxInput',
dataViewValue
);
await this.assertChangePointChartEmbeddableDataViewSelection(dataViewValue);
},
};
}

View file

@ -196,5 +196,6 @@
"@kbn/server-route-repository-utils",
"@kbn/streams-plugin",
"@kbn/response-ops-rule-params",
"@kbn/aiops-change-point-detection",
]
}