Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -126,7 +121,7 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
</EuiFlyoutHeader>

<EuiFlyoutBody>
<EuiForm>
<EuiForm data-test-subj="aiopsChangePointDetectionControls">
<ViewTypeSelector value={viewType} onChange={setViewType} />
<EuiFormRow
fullWidth
Expand All @@ -148,21 +143,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>
Expand Down Expand Up @@ -211,12 +205,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);

Expand Down Expand Up @@ -265,33 +254,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={
Expand Down Expand Up @@ -332,6 +298,6 @@ export const FormControls: FC<{
onChange={(v) => updateCallback({ maxSeriesToPlot: v })}
onValidationChange={(result) => onValidationChange(result === null)}
/>
</FieldStatsFlyoutProvider>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -24,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});

after(async () => {
await ml.testResources.deleteDashboardByTitle(dashboardTitle);
await ml.testResources.deleteDataViewByTitle('ft_ecommerce');
});

Expand All @@ -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);
});
});
}
74 changes: 70 additions & 4 deletions x-pack/test/functional/services/aiops/dashboard_embeddables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
}
});
},

Expand Down Expand Up @@ -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();
Expand All @@ -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'
Expand All @@ -99,12 +147,30 @@ 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',
dataViewValue
);
await this.assertLogRateAnalysisEmbeddableDataViewSelection(dataViewValue);
},

async selectChangePointChartEmbeddableDataView(dataViewValue: string) {
await comboBox.set(
'aiopsChangePointChartEmbeddableDataViewSelector > comboBoxInput',
dataViewValue
);
await this.assertChangePointChartEmbeddableDataViewSelection(dataViewValue);
},
};
}
1 change: 1 addition & 0 deletions x-pack/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,6 @@
"@kbn/ai-assistant-common",
"@kbn/core-deprecations-common",
"@kbn/usage-collection-plugin",
"@kbn/aiops-change-point-detection"
]
}