[ML] Add table layout for Change Point Detection embeddable (#174348)

## Summary

Part of #161248 

- Adds new "View type" parameter to the Change Point Detection
embeddable that allows choosing between charts and table layout
<img width="616" alt="image"
src="4a6580d5-0d92-41c4-9b07-dc430c52a87c">

<img width="1640" alt="image"
src="2b46c1f1-ce10-455b-9d0f-289635fa6b0d">

- Set the view type parameter while attaching from the ML app 
<img width="1289" alt="image"
src="b6a53c68-c5e2-4b77-be00-79fbdf37d90d">

- Allows attachment of a change point table to a Case 
<img width="1265" alt="image"
src="4dbe9738-0bca-4bff-ba13-ed2e4be5bef5">


- Fixes reporting on the loading and completed render states


### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Dima Arnautov 2024-01-15 11:52:53 +01:00 committed by GitHub
parent 9762fa3cb6
commit 13981d2211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 469 additions and 171 deletions

View file

@ -39,10 +39,11 @@ export interface UseTableState<T extends object> {
export function useTableState<T extends object>(
items: T[],
initialSortField: string,
initialSortDirection: 'asc' | 'desc' = 'asc'
initialSortDirection: 'asc' | 'desc' = 'asc',
initialPagionation?: Partial<Pagination>
) {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(initialPagionation?.pageIndex ?? 0);
const [pageSize, setPageSize] = useState(initialPagionation?.pageSize ?? 10);
const [sortField, setSortField] = useState<string>(initialSortField);
const [sortDirection, setSortDirection] = useState<Direction>(initialSortDirection);
@ -63,7 +64,7 @@ export function useTableState<T extends object>(
pageIndex,
pageSize,
totalItemCount: (items ?? []).length,
pageSizeOptions: [10, 20, 50],
pageSizeOptions: initialPagionation?.pageSizeOptions ?? [10, 20, 50],
showPerPageOptions: true,
};

View file

@ -26,9 +26,19 @@ export const CASES_ATTACHMENT_CHANGE_POINT_CHART = 'aiopsChangePointChart';
export const EMBEDDABLE_CHANGE_POINT_CHART_TYPE = 'aiopsChangePointChart' as const;
export type EmbeddableChangePointType = typeof EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
export const AIOPS_TELEMETRY_ID = {
AIOPS_DEFAULT_SOURCE: 'ml_aiops_labs',
AIOPS_ANALYSIS_RUN_ORIGIN: 'aiops-analysis-run-origin',
} as const;
export const EMBEDDABLE_ORIGIN = 'embeddable';
export const CHANGE_POINT_DETECTION_VIEW_TYPE = {
CHARTS: 'charts',
TABLE: 'table',
} as const;
export type ChangePointDetectionViewType =
typeof CHANGE_POINT_DETECTION_VIEW_TYPE[keyof typeof CHANGE_POINT_DETECTION_VIEW_TYPE];

View file

@ -45,7 +45,7 @@ export const initComponent = memoize(
return (
<>
<EuiDescriptionList compressed type={'inline'} listItems={listItems} />
<EmbeddableComponent {...inputProps} />
<EmbeddableComponent {...inputProps} embeddingOrigin={'cases'} />
</>
);
},

View file

@ -10,7 +10,10 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../../common/constants';
import {
CASES_ATTACHMENT_CHANGE_POINT_CHART,
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
} from '../../common/constants';
import { getEmbeddableChangePointChart } from '../embeddable/embeddable_change_point_chart_component';
import { AiopsPluginStartDeps } from '../types';
@ -19,7 +22,11 @@ export function registerChangePointChartsAttachment(
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps
) {
const EmbeddableComponent = getEmbeddableChangePointChart(coreStart, pluginStart);
const EmbeddableComponent = getEmbeddableChangePointChart(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
coreStart,
pluginStart
);
cases.attachmentFramework.registerPersistableState({
id: CASES_ATTACHMENT_CHANGE_POINT_CHART,

View file

@ -7,35 +7,37 @@
import {
EuiBadge,
type EuiBasicTableColumn,
EuiEmptyPrompt,
EuiIcon,
EuiInMemoryTable,
EuiToolTip,
type DefaultItemAction,
type EuiBasicTableColumn,
} from '@elastic/eui';
import React, { type FC, useMemo } from 'react';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { FilterStateStore, type Filter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { type Filter, FilterStateStore } from '@kbn/es-query';
import { NoChangePointsWarning } from './no_change_points_warning';
import { useTableState } from '@kbn/ml-in-memory-table';
import React, { useCallback, useEffect, useMemo, useRef, type FC } from 'react';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useDataSource } from '../../hooks/use_data_source';
import { useCommonChartProps } from './use_common_chart_props';
import {
type ChangePointAnnotation,
FieldConfig,
SelectedChangePoint,
useChangePointDetectionContext,
type ChangePointAnnotation,
} from './change_point_detection_context';
import { type ChartComponentProps } from './chart_component';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { NoChangePointsWarning } from './no_change_points_warning';
import { useCommonChartProps } from './use_common_chart_props';
export interface ChangePointsTableProps {
annotations: ChangePointAnnotation[];
fieldConfig: FieldConfig;
isLoading: boolean;
onSelectionChange: (update: SelectedChangePoint[]) => void;
onSelectionChange?: (update: SelectedChangePoint[]) => void;
onRenderComplete?: () => void;
}
function getFilterConfig(
@ -68,31 +70,62 @@ function getFilterConfig(
};
}
const pageSizeOptions = [5, 10, 15];
export const ChangePointsTable: FC<ChangePointsTableProps> = ({
isLoading,
annotations,
fieldConfig,
onSelectionChange,
onRenderComplete,
}) => {
const {
fieldFormats,
data: {
query: { filterManager },
},
embeddingOrigin,
} = useAiopsAppContext();
const { dataView } = useDataSource();
const chartLoadingCount = useRef<number>(0);
const { onTableChange, pagination, sorting } = useTableState<ChangePointAnnotation>(
annotations ?? [],
'p_value',
'asc',
{
pageIndex: 0,
pageSize: 10,
pageSizeOptions,
}
);
const dateFormatter = useMemo(() => fieldFormats.deserialize({ id: 'date' }), [fieldFormats]);
const defaultSorting = {
sort: {
field: 'p_value',
// Lower p_value indicates a bigger change point, hence the asc sorting
direction: 'asc' as const,
},
};
useEffect(() => {
// Reset loading counter on pagination or sort change
chartLoadingCount.current = 0;
}, [pagination.pageIndex, pagination.pageSize, sorting.sort]);
const hasActions = fieldConfig.splitField !== undefined;
/**
* Callback to track render of each chart component
* to report when all charts on the current page are ready.
*/
const onChartRenderCompleteCallback = useCallback(
(isLoadingChart: boolean) => {
if (!onRenderComplete) return;
if (!isLoadingChart) {
chartLoadingCount.current++;
}
if (chartLoadingCount.current === pagination.pageSize) {
onRenderComplete();
}
},
[onRenderComplete, pagination.pageSize]
);
const hasActions = fieldConfig.splitField !== undefined && embeddingOrigin !== 'cases';
const { bucketInterval } = useChangePointDetectionContext();
@ -131,6 +164,7 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
annotation={annotation}
fieldConfig={fieldConfig}
interval={bucketInterval.expression}
onRenderComplete={onChartRenderCompleteCallback.bind(null, false)}
/>
);
},
@ -190,70 +224,83 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
truncateText: false,
sortable: true,
},
{
name: i18n.translate('xpack.aiops.changePointDetection.actionsColumn', {
defaultMessage: 'Actions',
}),
actions: [
{
name: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterForValueAction',
{
defaultMessage: 'Filter for value',
}
),
description: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterForValueAction',
{
defaultMessage: 'Filter for value',
}
),
icon: 'plusInCircle',
color: 'primary',
type: 'icon',
onClick: (item) => {
filterManager.addFilters(
getFilterConfig(dataView.id!, item as Required<ChangePointAnnotation>, false)!
);
...(hasActions
? [
{
name: i18n.translate('xpack.aiops.changePointDetection.actionsColumn', {
defaultMessage: 'Actions',
}),
actions: [
{
name: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterForValueAction',
{
defaultMessage: 'Filter for value',
}
),
description: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterForValueAction',
{
defaultMessage: 'Filter for value',
}
),
icon: 'plusInCircle',
color: 'primary',
type: 'icon',
onClick: (item) => {
filterManager.addFilters(
getFilterConfig(
dataView.id!,
item as Required<ChangePointAnnotation>,
false
)!
);
},
isPrimary: true,
'data-test-subj': 'aiopsChangePointFilterForValue',
},
{
name: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterOutValueAction',
{
defaultMessage: 'Filter out value',
}
),
description: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterOutValueAction',
{
defaultMessage: 'Filter out value',
}
),
icon: 'minusInCircle',
color: 'primary',
type: 'icon',
onClick: (item) => {
filterManager.addFilters(
getFilterConfig(
dataView.id!,
item as Required<ChangePointAnnotation>,
true
)!
);
},
isPrimary: true,
'data-test-subj': 'aiopsChangePointFilterOutValue',
},
] as Array<DefaultItemAction<ChangePointAnnotation>>,
},
isPrimary: true,
'data-test-subj': 'aiopsChangePointFilterForValue',
},
{
name: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterOutValueAction',
{
defaultMessage: 'Filter out value',
}
),
description: i18n.translate(
'xpack.aiops.changePointDetection.actions.filterOutValueAction',
{
defaultMessage: 'Filter out value',
}
),
icon: 'minusInCircle',
color: 'primary',
type: 'icon',
onClick: (item) => {
filterManager.addFilters(
getFilterConfig(dataView.id!, item as Required<ChangePointAnnotation>, true)!
);
},
isPrimary: true,
'data-test-subj': 'aiopsChangePointFilterOutValue',
},
] as Array<DefaultItemAction<ChangePointAnnotation>>,
},
]
: []),
]
: []),
];
const selectionValue = useMemo<EuiTableSelectionType<ChangePointAnnotation>>(() => {
const selectionValue = useMemo<EuiTableSelectionType<ChangePointAnnotation> | undefined>(() => {
if (!onSelectionChange) return;
return {
selectable: (item) => true,
onSelectionChange: (selection) => {
onSelectionChange(
onSelectionChange!(
selection.map((s) => {
return {
...s,
@ -273,8 +320,11 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
data-test-subj={`aiopsChangePointResultsTable ${isLoading ? 'loading' : 'loaded'}`}
items={annotations}
columns={columns}
pagination={{ pageSizeOptions: [5, 10, 15] }}
sorting={defaultSorting}
pagination={
pagination.pageSizeOptions![0] > pagination!.totalItemCount ? undefined : pagination
}
sorting={sorting}
onTableChange={onTableChange}
hasActions={hasActions}
rowProps={(item) => ({
'data-test-subj': `aiopsChangePointResultsTableRow row-${item.id}`,
@ -300,7 +350,12 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
);
};
export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotation }) => {
export const MiniChartPreview: FC<ChartComponentProps> = ({
fieldConfig,
annotation,
onRenderComplete,
onLoading,
}) => {
const {
lens: { EmbeddableComponent },
} = useAiopsAppContext();
@ -314,8 +369,31 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotat
bucketInterval: bucketInterval.expression,
});
const chartWrapperRef = useRef<HTMLDivElement>(null);
const renderCompleteListener = useCallback(
(event: Event) => {
if (event.target === chartWrapperRef.current) return;
if (onRenderComplete) {
onRenderComplete();
}
},
[onRenderComplete]
);
useEffect(() => {
if (!chartWrapperRef.current) {
throw new Error('Reference to the chart wrapper is not set');
}
const chartWrapper = chartWrapperRef.current;
chartWrapper.addEventListener('renderComplete', renderCompleteListener);
return () => {
chartWrapper.removeEventListener('renderComplete', renderCompleteListener);
};
}, [renderCompleteListener]);
return (
<div data-test-subj={'aiopChangePointPreviewChart'}>
<div data-test-subj={'aiopChangePointPreviewChart'} ref={chartWrapperRef}>
<EmbeddableComponent
id={`mini_changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
style={{ height: 80 }}
@ -329,6 +407,7 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotat
type: 'aiops_change_point_detection_chart',
name: 'Change point detection',
}}
onLoad={onLoading}
/>
</div>
);

View file

@ -33,7 +33,11 @@ import {
import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu';
import { isDefined } from '@kbn/ml-is-defined';
import { MaxSeriesControl } from './max_series_control';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../../common/constants';
import {
ChangePointDetectionViewType,
CHANGE_POINT_DETECTION_VIEW_TYPE,
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
} from '../../../common/constants';
import { useCasesModal } from '../../hooks/use_cases_modal';
import { type EmbeddableChangePointChartInput } from '../../embeddable/embeddable_change_point_chart';
import { useDataSource } from '../../hooks/use_data_source';
@ -51,6 +55,7 @@ import {
} from './change_point_detection_context';
import { useChangePointResults } from './use_change_point_agg_request';
import { useSplitFieldCardinality } from './use_split_field_cardinality';
import { ViewTypeSelector } from './view_type_selector';
const selectControlCss = { width: '350px' };
@ -191,10 +196,17 @@ const FieldPanel: FC<FieldPanelProps> = ({
const [dashboardAttachment, setDashboardAttachment] = useState<{
applyTimeRange: boolean;
maxSeriesToPlot: number;
viewType: ChangePointDetectionViewType;
}>({
applyTimeRange: false,
maxSeriesToPlot: 6,
viewType: CHANGE_POINT_DETECTION_VIEW_TYPE.CHARTS,
});
const [caseAttachment, setCaseAttachment] = useState<{
viewType: ChangePointDetectionViewType;
}>({ viewType: CHANGE_POINT_DETECTION_VIEW_TYPE.CHARTS });
const [dashboardAttachmentReady, setDashboardAttachmentReady] = useState<boolean>(false);
const {
@ -294,20 +306,7 @@ const FieldPanel: FC<FieldPanelProps> = ({
}
: {}),
'data-test-subj': 'aiopsChangePointDetectionAttachToCaseButton',
onClick: () => {
openCasesModalCallback({
timeRange,
fn: fieldConfig.fn,
metricField: fieldConfig.metricField,
dataViewId: dataView.id,
...(fieldConfig.splitField
? {
splitField: fieldConfig.splitField,
partitions: selectedPartitions,
}
: {}),
});
},
panel: 'attachToCasePanel',
},
]
: []),
@ -324,6 +323,17 @@ const FieldPanel: FC<FieldPanelProps> = ({
<EuiPanel paddingSize={'s'}>
<EuiSpacer size={'s'} />
<EuiForm data-test-subj="aiopsChangePointDetectionDashboardAttachmentForm">
<ViewTypeSelector
value={dashboardAttachment.viewType}
onChange={(v) => {
setDashboardAttachment((prevState) => {
return {
...prevState,
viewType: v,
};
});
}}
/>
<EuiFormRow fullWidth>
<EuiSwitch
label={i18n.translate('xpack.aiops.changePointDetection.applyTimeRangeLabel', {
@ -366,7 +376,63 @@ const FieldPanel: FC<FieldPanelProps> = ({
fill
type={'submit'}
fullWidth
onClick={setDashboardAttachmentReady.bind(null, true)}
onClick={() => {
setIsActionMenuOpen(false);
setDashboardAttachmentReady(true);
}}
disabled={!isDashboardFormValid}
>
<FormattedMessage
id="xpack.aiops.changePointDetection.submitDashboardAttachButtonLabel"
defaultMessage="Attach"
/>
</EuiButton>
</EuiForm>
</EuiPanel>
),
},
{
id: 'attachToCasePanel',
title: i18n.translate('xpack.aiops.changePointDetection.attachToCaseTitle', {
defaultMessage: 'Attach to case',
}),
size: 's',
content: (
<EuiPanel paddingSize={'s'}>
<EuiSpacer size={'s'} />
<EuiForm data-test-subj="aiopsChangePointDetectionCasedAttachmentForm">
<ViewTypeSelector
value={caseAttachment.viewType}
onChange={(v) => {
setCaseAttachment((prevState) => {
return {
...prevState,
viewType: v,
};
});
}}
/>
<EuiButton
data-test-subj="aiopsChangePointDetectionSubmitCaseAttachButton"
fill
type={'submit'}
fullWidth
onClick={() => {
setIsActionMenuOpen(false);
openCasesModalCallback({
timeRange,
viewType: caseAttachment.viewType,
fn: fieldConfig.fn,
metricField: fieldConfig.metricField,
dataViewId: dataView.id,
...(fieldConfig.splitField
? {
splitField: fieldConfig.splitField,
partitions: selectedPartitions,
}
: {}),
});
}}
disabled={!isDashboardFormValid}
>
<FormattedMessage
@ -383,9 +449,11 @@ const FieldPanel: FC<FieldPanelProps> = ({
canCreateCase,
canEditDashboards,
canUpdateCase,
caseAttachment.viewType,
caseAttachmentButtonDisabled,
dashboardAttachment.applyTimeRange,
dashboardAttachment.maxSeriesToPlot,
dashboardAttachment.viewType,
dataView.id,
fieldConfig.fn,
fieldConfig.metricField,
@ -405,6 +473,7 @@ const FieldPanel: FC<FieldPanelProps> = ({
const embeddableInput: Partial<EmbeddableChangePointChartInput> = {
title: newTitle,
description: newDescription,
viewType: dashboardAttachment.viewType,
dataViewId: dataView.id,
metricField: fieldConfig.metricField,
splitField: fieldConfig.splitField,
@ -428,12 +497,13 @@ const FieldPanel: FC<FieldPanelProps> = ({
},
[
embeddable,
dashboardAttachment.viewType,
dashboardAttachment.applyTimeRange,
dashboardAttachment.maxSeriesToPlot,
dataView.id,
fieldConfig.metricField,
fieldConfig.splitField,
fieldConfig.fn,
dashboardAttachment.applyTimeRange,
dashboardAttachment.maxSeriesToPlot,
timeRange,
selectedChangePoints,
panelIndex,

View file

@ -6,7 +6,14 @@
*/
import React, { type FC, useState, useCallback, useMemo, useEffect } from 'react';
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
import {
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIcon,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
@ -171,9 +178,26 @@ export const PartitionsSelector: FC<PartitionsSelectorProps> = ({
return (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.aiops.changePointDetection.partitionsLabel', {
defaultMessage: 'Partitions',
})}
label={
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.aiops.changePointDetection.partitionsLabel', {
defaultMessage: 'Partitions',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate('xpack.aiops.changePointDetection.partitionsDescription', {
defaultMessage:
'If not supplied, the largest change points across all split field values will be displayed.',
})}
position="right"
>
<EuiIcon size="s" type="questionInCircle" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiComboBox<string>
isLoading={isLoading}

View file

@ -136,7 +136,7 @@ export function useChangePointResults(
/**
* null also means the fetching has been complete
*/
const [progress, setProgress] = useState<number | null>(null);
const [progress, setProgress] = useState<number | null>(0);
const isSingleMetric = !isDefined(fieldConfig.splitField);

View file

@ -0,0 +1,59 @@
/*
* 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 React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonGroup, EuiFormRow, type EuiButtonGroupOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ChangePointDetectionViewType } from '../../../common/constants';
const viewTypeOptions: EuiButtonGroupOptionProps[] = [
{
id: `charts`,
label: (
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.viewTypeSelector.chartsLabel"
defaultMessage="Charts"
/>
),
iconType: 'visLine',
},
{
id: `table`,
label: (
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.viewTypeSelector.tableLabel"
defaultMessage="Table"
/>
),
iconType: 'visTable',
},
];
export interface ViewTypeSelectorProps {
value: ChangePointDetectionViewType;
onChange: (update: ChangePointDetectionViewType) => void;
}
export const ViewTypeSelector: FC<ViewTypeSelectorProps> = ({ value, onChange }) => {
return (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.aiops.embeddableChangePointChart.viewTypeLabel', {
defaultMessage: 'View type',
})}
>
<EuiButtonGroup
isFullWidth
legend="This is a basic group"
options={viewTypeOptions}
idSelected={value}
onChange={onChange as (id: string) => void}
/>
</EuiFormRow>
);
};

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
@ -18,28 +17,30 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import usePrevious from 'react-use/lib/usePrevious';
import { pick } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { PartitionsSelector } from '../components/change_point_detection/partitions_selector';
import { DEFAULT_SERIES } from './const';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
import { type EmbeddableChangePointChartExplicitInput } from './types';
import { MaxSeriesControl } from '../components/change_point_detection/max_series_control';
import { SplitFieldSelector } from '../components/change_point_detection/split_field_selector';
import { MetricFieldSelector } from '../components/change_point_detection/metric_field_selector';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { pick } from 'lodash';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import {
ChangePointDetectionControlsContextProvider,
useChangePointDetectionControlsContext,
} from '../components/change_point_detection/change_point_detection_context';
import { useAiopsAppContext } from '../hooks/use_aiops_app_context';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import { FunctionPicker } from '../components/change_point_detection/function_picker';
import { DataSourceContextProvider } from '../hooks/use_data_source';
import { DEFAULT_AGG_FUNCTION } from '../components/change_point_detection/constants';
import { FunctionPicker } from '../components/change_point_detection/function_picker';
import { MaxSeriesControl } from '../components/change_point_detection/max_series_control';
import { MetricFieldSelector } from '../components/change_point_detection/metric_field_selector';
import { PartitionsSelector } from '../components/change_point_detection/partitions_selector';
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 { DataSourceContextProvider } from '../hooks/use_data_source';
import { DEFAULT_SERIES } from './const';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
import { type EmbeddableChangePointChartExplicitInput } from './types';
export interface AnomalyChartsInitializerProps {
initialInput?: Partial<EmbeddableChangePointChartInput>;
@ -59,6 +60,7 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
} = useAiopsAppContext();
const [dataViewId, setDataViewId] = useState(initialInput?.dataViewId ?? '');
const [viewType, setViewType] = useState(initialInput?.viewType ?? 'charts');
const [formInput, setFormInput] = useState<FormControlsProps>(
pick(initialInput ?? {}, [
@ -75,6 +77,7 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
const updatedProps = useMemo(() => {
return {
...formInput,
viewType,
title: isPopulatedObject(formInput)
? i18n.translate('xpack.aiops.changePointDetection.attachmentTitle', {
defaultMessage: 'Change point: {function}({metric}){splitBy}',
@ -92,7 +95,7 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
: '',
dataViewId,
};
}, [formInput, dataViewId]);
}, [formInput, dataViewId, viewType]);
return (
<EuiModal onClose={onCancel} data-test-subj={'aiopsChangePointChartEmbeddableInitializer'}>
@ -100,13 +103,14 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.modalTitle"
defaultMessage="Change point charts configuration"
defaultMessage="Change point detection configuration"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiForm>
<ViewTypeSelector value={viewType} onChange={setViewType} />
<EuiFormRow
fullWidth
label={i18n.translate('xpack.aiops.embeddableChangePointChart.dataViewLabel', {
@ -129,7 +133,6 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
}}
/>
</EuiFormRow>
<DataSourceContextProvider dataViewId={dataViewId}>
<EuiHorizontalRule margin={'s'} />
<ChangePointDetectionControlsContextProvider>

View file

@ -23,8 +23,9 @@ import { LensPublicStart } from '@kbn/lens-plugin/public';
import { Subject } from 'rxjs';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { EmbeddableInputTracker } from './embeddable_chart_component_wrapper';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE, EMBEDDABLE_ORIGIN } from '../../common/constants';
import { EMBEDDABLE_ORIGIN, EmbeddableChangePointType } from '../../common/constants';
import { AiopsAppContext, type AiopsAppDependencies } from '../hooks/use_aiops_app_context';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
@ -42,6 +43,7 @@ export interface EmbeddableChangePointChartDeps {
i18n: CoreStart['i18n'];
lens: LensPublicStart;
usageCollection: UsageCollectionSetup;
fieldFormats: FieldFormatsStart;
}
export type IEmbeddableChangePointChart = typeof EmbeddableChangePointChart;
@ -50,8 +52,6 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
EmbeddableChangePointChartInput,
EmbeddableChangePointChartOutput
> {
public readonly type = EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
private reload$ = new Subject<number>();
public reload(): void {
@ -64,6 +64,7 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
deferEmbeddableLoad = true;
constructor(
public readonly type: EmbeddableChangePointType,
private readonly deps: EmbeddableChangePointChartDeps,
initialInput: EmbeddableChangePointChartInput,
parent?: IContainer
@ -91,9 +92,9 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
return true;
}
public onLoading() {
public onLoading(isLoading: boolean) {
this.renderComplete.dispatchInProgress();
this.updateOutput({ loading: true, error: undefined });
this.updateOutput({ loading: isLoading, error: undefined });
}
public onError(error: Error) {
@ -103,7 +104,7 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
public onRenderComplete() {
this.renderComplete.dispatchComplete();
this.updateOutput({ loading: false, error: undefined });
this.updateOutput({ loading: false, rendered: true, error: undefined });
}
render(el: HTMLElement): void {
@ -127,8 +128,8 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
const input$ = this.getInput$();
const aiopsAppContextValue = {
embeddingOrigin: this.parent?.type ?? input.embeddingOrigin ?? EMBEDDABLE_ORIGIN,
...this.deps,
embeddingOrigin: this.parent?.type ?? EMBEDDABLE_ORIGIN,
} as unknown as AiopsAppDependencies;
ReactDOM.render(

View file

@ -15,12 +15,16 @@ import {
useEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { EuiLoadingChart } from '@elastic/eui';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import {
type ChangePointDetectionViewType,
type EmbeddableChangePointType,
} from '../../common/constants';
import type { AiopsPluginStartDeps } from '../types';
import type { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import type { ChangePointAnnotation } from '../components/change_point_detection/change_point_detection_context';
export interface EmbeddableChangePointChartProps {
viewType?: ChangePointDetectionViewType;
dataViewId: string;
timeRange: TimeRange;
fn: 'avg' | 'sum' | 'min' | 'max' | string;
@ -40,12 +44,16 @@ export interface EmbeddableChangePointChartProps {
* Last reload request time, can be used for manual reload
*/
lastReloadRequestTime?: number;
/** Origin of the embeddable instance */
embeddingOrigin?: string;
}
export function getEmbeddableChangePointChart(core: CoreStart, plugins: AiopsPluginStartDeps) {
export function getEmbeddableChangePointChart(
visType: EmbeddableChangePointType,
core: CoreStart,
plugins: AiopsPluginStartDeps
) {
const { embeddable: embeddableStart } = plugins;
const factory = embeddableStart.getEmbeddableFactory<EmbeddableChangePointChartInput>(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE
)!;
const factory = embeddableStart.getEmbeddableFactory<EmbeddableChangePointChartInput>(visType)!;
return (props: EmbeddableChangePointChartProps) => {
const input = { ...props };

View file

@ -13,7 +13,10 @@ import {
import { i18n } from '@kbn/i18n';
import { type DataPublicPluginStart } from '@kbn/data-plugin/public';
import { StartServicesAccessor } from '@kbn/core-lifecycle-browser';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import {
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
EmbeddableChangePointType,
} from '../../common/constants';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types';
import {
EmbeddableChangePointChart,
@ -27,8 +30,6 @@ export interface EmbeddableChangePointChartStartServices {
export type EmbeddableChangePointChartType = typeof EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefinition {
public readonly type = EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
public readonly grouping = [
{
id: 'ml',
@ -41,6 +42,8 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
];
constructor(
public readonly type: EmbeddableChangePointType,
private readonly name: string,
private readonly getStartServices: StartServicesAccessor<AiopsPluginStartDeps, AiopsPluginStart>
) {}
@ -49,9 +52,7 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
};
getDisplayName() {
return i18n.translate('xpack.aiops.embeddableChangePointChartDisplayName', {
defaultMessage: 'Change point detection',
});
return this.name;
}
canCreateNew() {
@ -73,10 +74,11 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
try {
const [
{ i18n: i18nService, theme, http, uiSettings, notifications },
{ lens, data, usageCollection },
{ lens, data, usageCollection, fieldFormats },
] = await this.getStartServices();
return new EmbeddableChangePointChart(
this.type,
{
theme,
http,
@ -86,6 +88,7 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
notifications,
lens,
usageCollection,
fieldFormats,
},
input,
parent

View file

@ -5,12 +5,14 @@
* 2.0.
*/
import { BehaviorSubject, type Observable, combineLatest } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, type Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
import { ChangePointsTable } from '../components/change_point_detection/change_points_table';
import { CHANGE_POINT_DETECTION_VIEW_TYPE } from '../../common/constants';
import { ReloadContextProvider } from '../hooks/use_reload';
import {
type ChangePointAnnotation,
@ -42,7 +44,7 @@ export interface EmbeddableInputTrackerProps {
reload$: Observable<number>;
onOutputChange: (output: Partial<EmbeddableChangePointChartOutput>) => void;
onRenderComplete: () => void;
onLoading: () => void;
onLoading: (isLoading: boolean) => void;
onError: (error: Error) => void;
}
@ -86,6 +88,7 @@ export const EmbeddableInputTracker: FC<EmbeddableInputTrackerProps> = ({
<ChangePointDetectionControlsContextProvider>
<FilterQueryContextProvider timeRange={input.timeRange}>
<ChartGridEmbeddableWrapper
viewType={input.viewType}
timeRange={input.timeRange}
fn={input.fn}
metricField={input.metricField}
@ -120,10 +123,11 @@ export const EmbeddableInputTracker: FC<EmbeddableInputTrackerProps> = ({
export const ChartGridEmbeddableWrapper: FC<
EmbeddableChangePointChartProps & {
onRenderComplete: () => void;
onLoading: () => void;
onLoading: (isLoading: boolean) => void;
onError: (error: Error) => void;
}
> = ({
viewType = CHANGE_POINT_DETECTION_VIEW_TYPE.CHARTS,
fn,
metricField,
maxSeriesToPlot,
@ -202,9 +206,7 @@ export const ChartGridEmbeddableWrapper: FC<
);
useEffect(() => {
if (isLoading) {
onLoading();
}
onLoading(isLoading);
}, [onLoading, isLoading]);
const changePoints = useMemo<ChangePointAnnotation[]>(() => {
@ -235,16 +237,27 @@ export const ChartGridEmbeddableWrapper: FC<
`}
>
{changePoints.length > 0 ? (
<ChartsGrid
changePoints={changePoints.map((r) => ({ ...r, ...fieldConfig }))}
interval={requestParams.interval}
onRenderComplete={onRenderComplete}
/>
) : emptyState ? (
emptyState
) : (
<NoChangePointsWarning onRenderComplete={onRenderComplete} />
)}
viewType === CHANGE_POINT_DETECTION_VIEW_TYPE.CHARTS ? (
<ChartsGrid
changePoints={changePoints.map((r) => ({ ...r, ...fieldConfig }))}
interval={requestParams.interval}
onRenderComplete={onRenderComplete}
/>
) : viewType === CHANGE_POINT_DETECTION_VIEW_TYPE.TABLE ? (
<ChangePointsTable
isLoading={false}
annotations={changePoints}
fieldConfig={fieldConfig}
onRenderComplete={onRenderComplete}
/>
) : null
) : !isLoading ? (
emptyState ? (
emptyState
) : (
<NoChangePointsWarning onRenderComplete={onRenderComplete} />
)
) : null}
</div>
);
};

View file

@ -7,6 +7,8 @@
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types';
import { EmbeddableChangePointChartFactory } from './embeddable_change_point_chart_factory';
@ -14,6 +16,12 @@ export const registerEmbeddable = (
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>,
embeddable: EmbeddableSetup
) => {
const factory = new EmbeddableChangePointChartFactory(core.getStartServices);
embeddable.registerEmbeddableFactory(factory.type, factory);
const changePointChartFactory = new EmbeddableChangePointChartFactory(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
i18n.translate('xpack.aiops.embeddableChangePointChartDisplayName', {
defaultMessage: 'Change point detection',
}),
core.getStartServices
);
embeddable.registerEmbeddableFactory(changePointChartFactory.type, changePointChartFactory);
};

View file

@ -5,7 +5,9 @@
* 2.0.
*/
import type { FC } from 'react';
import { IEmbeddable } from '@kbn/embeddable-plugin/public';
import type { SelectedChangePoint } from '../components/change_point_detection/change_point_detection_context';
import {
EmbeddableChangePointChartInput,
EmbeddableChangePointChartOutput,
@ -19,3 +21,9 @@ export type EmbeddableChangePointChartExplicitInput = {
export interface EditChangePointChartsPanelContext {
embeddable: IEmbeddable<EmbeddableChangePointChartInput, EmbeddableChangePointChartOutput>;
}
export type ViewComponent = FC<{
changePoints: SelectedChangePoint[];
interval: string;
onRenderComplete?: () => void;
}>;

View file

@ -8,6 +8,7 @@
import type { CoreStart, Plugin } from '@kbn/core/public';
import { type CoreSetup } from '@kbn/core/public';
import { firstValueFrom } from 'rxjs';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../common/constants';
import type {
AiopsPluginSetup,
AiopsPluginSetupDeps,
@ -58,7 +59,11 @@ export class AiopsPlugin
public start(core: CoreStart, plugins: AiopsPluginStartDeps): AiopsPluginStart {
return {
EmbeddableChangePointChart: getEmbeddableChangePointChart(core, plugins),
EmbeddableChangePointChart: getEmbeddableChangePointChart(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
core,
plugins
),
};
}

View file

@ -11,13 +11,12 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { EmbeddableChangePointChartInput } from './embeddable/embeddable_change_point_chart';

View file

@ -60,7 +60,7 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable<
public onRenderComplete() {
this.renderComplete.dispatchComplete();
this.updateOutput({ loading: false, error: undefined });
this.updateOutput({ loading: false, rendered: true, error: undefined });
}
public render(node: HTMLElement) {

View file

@ -65,7 +65,7 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
public onRenderComplete() {
this.renderComplete.dispatchComplete();
this.updateOutput({ loading: false, error: undefined });
this.updateOutput({ loading: false, rendered: true, error: undefined });
}
public render(node: HTMLElement) {