diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap index 5bb924a71a2a1..bb2fe700f6f19 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap @@ -49,6 +49,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "arc", "ticksPosition": "auto", }, @@ -100,6 +101,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "arc", "ticksPosition": "auto", }, @@ -149,6 +151,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -198,6 +201,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -247,6 +251,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "bands", }, @@ -298,6 +303,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "circle", "ticksPosition": "auto", }, @@ -349,6 +355,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "circle", "ticksPosition": "auto", }, @@ -398,6 +405,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -447,6 +455,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "hidden", }, @@ -496,6 +505,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -545,6 +555,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -594,6 +605,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -643,6 +655,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "horizontalBullet", "ticksPosition": "auto", }, @@ -692,6 +705,7 @@ Object { "min": "col-1-2", "palette": undefined, "percentageMode": false, + "respectRanges": false, "shape": "verticalBullet", "ticksPosition": "auto", }, diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts index ba40821948526..c2ce20163e86a 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts @@ -160,6 +160,13 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({ defaultMessage: 'Enables relative precentage mode', }), }, + respectRanges: { + types: ['boolean'], + default: false, + help: i18n.translate('expressionGauge.functions.gauge.respectRanges.help', { + defaultMessage: 'Respect max and min values from ranges', + }), + }, commonLabel: { types: ['string'], help: i18n.translate('expressionGauge.functions.gauge.args.commonLabel.help', { @@ -173,7 +180,6 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({ }), }, }, - fn(data, args, handlers) { validateAccessor(args.metric, data.columns); validateAccessor(args.min, data.columns); diff --git a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts index b6add118a6747..b2696acda6c7d 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts @@ -47,6 +47,7 @@ export interface GaugeState { shape: GaugeShape; /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ percentageMode?: boolean; + respectRanges?: boolean; commonLabel?: string; } diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx index 8258063dfdd69..efaea7dd24954 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx +++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx @@ -303,8 +303,8 @@ export const GaugeComponent: FC = memo( } const goal = accessors.goal ? getValueFromAccessor(accessors.goal, row) : undefined; - const min = getMinValue(row, accessors); - const max = getMaxValue(row, accessors); + const min = getMinValue(row, accessors, palette?.params, args.respectRanges); + const max = getMaxValue(row, accessors, palette?.params, args.respectRanges); if (min === max) { return ( diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.test.ts b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.test.ts index 966916e8bacaf..10100ca84065b 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.test.ts +++ b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.test.ts @@ -46,6 +46,10 @@ describe('expression gauge utils', () => { expect(getMaxValue({ min: 0 }, localAccessors)).toEqual(100); expect(getMaxValue({}, localAccessors)).toEqual(100); }); + it('returns correct value if isRespectRanges is true and color palette was provided', () => { + expect(getMaxValue({ metric: 10 }, accessors, { rangeMax: 5 }, true)).toEqual(10); + expect(getMaxValue({ metric: 2 }, accessors, { rangeMax: 5 }, true)).toEqual(5); + }); it('returns correct value for multiple cases', () => { const localAccessors = { ...accessors, max: 'max' }; expect(getMaxValue({ metric: 10 }, localAccessors)).toEqual(15); @@ -76,6 +80,13 @@ describe('expression gauge utils', () => { expect(getMinValue({ metric: -1000 }, localAccessors)).toEqual(-1010); expect(getMinValue({ max: 1000, metric: 1.5 }, localAccessors)).toEqual(0); }); + it('returns correct value if isRespectRanges is true and color palette was provided', () => { + const accessors = { + metric: 'metric', + }; + expect(getMinValue({ metric: 10 }, accessors, { rangeMin: 5 }, true)).toEqual(5); + expect(getMinValue({ metric: 2 }, accessors, { rangeMin: 5 }, true)).toEqual(2); + }); }); describe('getGoalValue', () => { it('returns correct value', () => { diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts index 8848c7646a5f0..31a2ff61ceaa7 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts +++ b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts @@ -8,7 +8,7 @@ import type { DatatableColumn, DatatableRow } from 'src/plugins/expressions'; import { getAccessorByDimension } from '../../../../../visualizations/common/utils'; -import { Accessors, GaugeArguments } from '../../../common'; +import { Accessors, GaugeArguments, CustomPaletteParams } from '../../../common'; export const getValueFromAccessor = ( accessor: string, @@ -54,17 +54,30 @@ function getNiceNumber(localRange: number) { return niceFraction * Math.pow(10, exponent); } -export const getMaxValue = (row?: DatatableRow, accessors?: Accessors): number => { +export const getMaxValue = ( + row?: DatatableRow, + accessors?: Accessors, + paletteParams?: CustomPaletteParams, + isRespectRanges?: boolean +): number => { const FALLBACK_VALUE = 100; const currentValue = accessors?.max ? getValueFromAccessor(accessors.max, row) : undefined; if (currentValue !== undefined && currentValue !== null) { return currentValue; } + + if (isRespectRanges && paletteParams?.rangeMax) { + const metricValue = accessors?.metric ? getValueFromAccessor(accessors.metric, row) : undefined; + return !metricValue || metricValue < paletteParams?.rangeMax + ? paletteParams?.rangeMax + : metricValue; + } + if (row && accessors) { const { metric, goal } = accessors; const metricValue = metric && row[metric]; const goalValue = goal && row[goal]; - const minValue = getMinValue(row, accessors); + const minValue = getMinValue(row, accessors, paletteParams, isRespectRanges); if (metricValue != null) { const numberValues = [minValue, goalValue, metricValue].filter((v) => typeof v === 'number'); const maxValue = Math.max(...numberValues); @@ -74,11 +87,24 @@ export const getMaxValue = (row?: DatatableRow, accessors?: Accessors): number = return FALLBACK_VALUE; }; -export const getMinValue = (row?: DatatableRow, accessors?: Accessors) => { +export const getMinValue = ( + row?: DatatableRow, + accessors?: Accessors, + paletteParams?: CustomPaletteParams, + isRespectRanges?: boolean +) => { const currentValue = accessors?.min ? getValueFromAccessor(accessors.min, row) : undefined; if (currentValue !== undefined && currentValue !== null) { return currentValue; } + + if (isRespectRanges && paletteParams?.rangeMin) { + const metricValue = accessors?.metric ? getValueFromAccessor(accessors.metric, row) : undefined; + return !metricValue || metricValue > paletteParams?.rangeMin + ? paletteParams?.rangeMin + : metricValue; + } + const FALLBACK_VALUE = 0; if (row && accessors) { const { metric, max } = accessors; diff --git a/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap index cd9ce5427fd6f..73c0ee3e38d7f 100644 --- a/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap @@ -47,6 +47,9 @@ Object { "some custom sublabel", ], "metric": Array [], + "respectRanges": Array [ + true, + ], "shape": Array [ "circle", ], diff --git a/src/plugins/vis_types/gauge/public/to_ast.ts b/src/plugins/vis_types/gauge/public/to_ast.ts index 1e9226aa10094..3728453b52583 100644 --- a/src/plugins/vis_types/gauge/public/to_ast.ts +++ b/src/plugins/vis_types/gauge/public/to_ast.ts @@ -72,6 +72,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) centralMajorMode, ...(centralMajorMode === 'custom' ? { labelMinor: style.subText } : {}), percentageMode, + respectRanges: true, commonLabel: schemas.metric?.[0]?.label, }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index eae099404d318..ffcb7a1abe416 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -260,6 +260,8 @@ export const AllCasesList = React.memo( }} setFilterRefetch={setFilterRefetch} hiddenStatuses={hiddenStatuses} + displayCreateCaseButton={isSelectorView} + onCreateCasePressed={onRowClick} /> { expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] }); }); + + describe('create case button', () => { + it('should not render the create case button when displayCreateCaseButton and onCreateCasePressed are not passed', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`).length).toBe(0); + }); + + it('should render the create case button when displayCreateCaseButton and onCreateCasePressed are passed', () => { + const onCreateCasePressed = jest.fn(); + const wrapper = mount( + + + + ); + expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`)).toBeTruthy(); + }); + + it('should call the onCreateCasePressed when create case is clicked', () => { + const onCreateCasePressed = jest.fn(); + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`).first().simulate('click'); + wrapper.update(); + // NOTE: intentionally checking no arguments are passed + expect(onCreateCasePressed).toHaveBeenCalledWith(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index f75cebf88933c..faee469d1c4bc 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup, EuiButton } from '@elastic/eui'; import { StatusAll, CaseStatusWithAllStatus } from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; @@ -17,8 +17,8 @@ import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; import { StatusFilter } from './status_filter'; - import * as i18n from './translations'; + interface CasesTableFiltersProps { countClosedCases: number | null; countInProgressCases: number | null; @@ -28,6 +28,8 @@ interface CasesTableFiltersProps { setFilterRefetch: (val: () => void) => void; hiddenStatuses?: CaseStatusWithAllStatus[]; availableSolutions: string[]; + displayCreateCaseButton?: boolean; + onCreateCasePressed?: () => void; } // Fix the width of the status dropdown to prevent hiding long text items @@ -61,6 +63,8 @@ const CasesTableFiltersComponent = ({ setFilterRefetch, hiddenStatuses, availableSolutions, + displayCreateCaseButton, + onCreateCasePressed, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map((r) => r.full_name ?? r.username ?? '') @@ -157,6 +161,12 @@ const CasesTableFiltersComponent = ({ [countClosedCases, countInProgressCases, countOpenCases] ); + const handleOnCreateCasePressed = useCallback(() => { + if (onCreateCasePressed) { + onCreateCasePressed(); + } + }, [onCreateCasePressed]); + return ( @@ -207,6 +217,18 @@ const CasesTableFiltersComponent = ({ )} + {displayCreateCaseButton && onCreateCasePressed ? ( + + + {i18n.CREATE_CASE_TITLE} + + + ) : null} ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 6bdd41d8db631..66e9ff5bbb416 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -294,7 +294,6 @@ describe('IndexPattern Data Source', () => { }, }, savedObjectReferences: [ - { name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' }, { name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' }, ], }); @@ -2705,14 +2704,7 @@ describe('IndexPattern Data Source', () => { }, }; - const currentIndexPatternReference = { - id: 'some-id', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }; - const references1: SavedObjectReference[] = [ - currentIndexPatternReference, { id: 'some-id', name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e', @@ -2721,7 +2713,6 @@ describe('IndexPattern Data Source', () => { ]; const references2: SavedObjectReference[] = [ - currentIndexPatternReference, { id: 'some-DIFFERENT-id', name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 77b0ac7de78ca..15391aaf95c80 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -589,7 +589,6 @@ describe('loader', () => { const state = await loadInitialState({ persistedState: savedState, references: [ - { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' }, { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], @@ -640,7 +639,6 @@ describe('loader', () => { const state = await loadInitialState({ persistedState: savedState, references: [ - { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' }, { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], @@ -727,11 +725,6 @@ describe('loader', () => { const state = await loadInitialState({ persistedState: savedState, references: [ - { - name: 'indexpattern-datasource-current-indexpattern', - id: 'conflictId', - type: 'index-pattern', - }, { name: 'indexpattern-datasource-layer-layerb', id: 'conflictId', type: 'index-pattern' }, ], indexPatternsService: mockIndexPatternsServiceWithConflict(), @@ -799,11 +792,6 @@ describe('loader', () => { const { savedObjectReferences } = extractReferences(state); expect(savedObjectReferences).toMatchInlineSnapshot(` Array [ - Object { - "id": "b", - "name": "indexpattern-datasource-current-indexpattern", - "type": "index-pattern", - }, Object { "id": "id-index-pattern-a", "name": "indexpattern-datasource-layer-a", @@ -822,13 +810,6 @@ describe('loader', () => { const { savedObjectReferences, state: persistedState } = extractReferences(state); expect(injectReferences(persistedState, savedObjectReferences).layers).toEqual(state.layers); }); - - it('should restore current index pattern', () => { - const { savedObjectReferences, state: persistedState } = extractReferences(state); - expect(injectReferences(persistedState, savedObjectReferences).currentIndexPatternId).toEqual( - state.currentIndexPatternId - ); - }); }); describe('changeIndexPattern', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 6742e92bbbf15..a8ad1885b3350 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -165,18 +165,12 @@ const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => { writeToStorage(storage, 'indexPatternId', value); }; -const CURRENT_PATTERN_REFERENCE_NAME = 'indexpattern-datasource-current-indexpattern'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; } -export function extractReferences({ currentIndexPatternId, layers }: IndexPatternPrivateState) { +export function extractReferences({ layers }: IndexPatternPrivateState) { const savedObjectReferences: SavedObjectReference[] = []; - savedObjectReferences.push({ - type: 'index-pattern', - id: currentIndexPatternId, - name: CURRENT_PATTERN_REFERENCE_NAME, - }); const persistableLayers: Record> = {}; Object.entries(layers).forEach(([layerId, { indexPatternId, ...persistableLayer }]) => { savedObjectReferences.push({ @@ -201,8 +195,6 @@ export function injectReferences( }; }); return { - currentIndexPatternId: references.find(({ name }) => name === CURRENT_PATTERN_REFERENCE_NAME)! - .id, layers, }; } @@ -246,13 +238,7 @@ export async function loadInitialState({ const usedPatterns = ( initialContext ? indexPatternIds - : uniq( - state - ? Object.values(state.layers) - .map((l) => l.indexPatternId) - .concat(state.currentIndexPatternId) - : [fallbackId] - ) + : uniq(state ? Object.values(state.layers).map((l) => l.indexPatternId) : [fallbackId]) ) // take out the undefined from the list .filter(Boolean); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index 88a153c1e0051..424c1df6497e2 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -94,10 +94,28 @@ export class AnomalyChartsEmbeddable extends Embeddable< } } + public onLoading() { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ loading: true, error: undefined }); + } + + public onError(error: Error) { + this.renderComplete.dispatchError(); + this.updateOutput({ loading: false, error: { name: error.name, message: error.message } }); + } + + public onRenderComplete() { + this.renderComplete.dispatchComplete(); + this.updateOutput({ loading: false, error: undefined }); + } + public render(node: HTMLElement) { super.render(node); this.node = node; + // required for the export feature to work + this.node.setAttribute('data-shared-item', ''); + const I18nContext = this.services[0].i18n.Context; const theme$ = this.services[0].theme.theme$; @@ -114,6 +132,9 @@ export class AnomalyChartsEmbeddable extends Embeddable< refresh={this.reload$.asObservable()} onInputChange={this.updateInput.bind(this)} onOutputChange={this.updateOutput.bind(this)} + onRenderComplete={this.onRenderComplete.bind(this)} + onLoading={this.onLoading.bind(this)} + onError={this.onError.bind(this)} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.test.tsx index efa89dd7e7608..9b38d67847388 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.test.tsx @@ -49,6 +49,9 @@ describe('EmbeddableAnomalyChartsContainer', () => { const onInputChange = jest.fn(); const onOutputChange = jest.fn(); + const onRenderComplete = jest.fn(); + const onLoading = jest.fn(); + const onError = jest.fn(); const mockedInput = { viewMode: 'view', @@ -145,6 +148,9 @@ describe('EmbeddableAnomalyChartsContainer', () => { refresh={refresh} onInputChange={onInputChange} onOutputChange={onOutputChange} + onLoading={onLoading} + onRenderComplete={onRenderComplete} + onError={onError} />, defaultOptions ); @@ -172,6 +178,9 @@ describe('EmbeddableAnomalyChartsContainer', () => { refresh={refresh} onInputChange={onInputChange} onOutputChange={onOutputChange} + onLoading={onLoading} + onRenderComplete={onRenderComplete} + onError={onError} />, defaultOptions ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx index 923014a5c4d4d..e3f8fb3dcdeff 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx @@ -38,6 +38,9 @@ export interface EmbeddableAnomalyChartsContainerProps { refresh: Observable; onInputChange: (input: Partial) => void; onOutputChange: (output: Partial) => void; + onRenderComplete: () => void; + onLoading: () => void; + onError: (error: Error) => void; } export const EmbeddableAnomalyChartsContainer: FC = ({ @@ -48,6 +51,9 @@ export const EmbeddableAnomalyChartsContainer: FC { const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( @@ -94,7 +100,8 @@ export const EmbeddableAnomalyChartsContainer: FC { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts index c104c5da80545..6aa148b18ce0c 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts @@ -40,6 +40,12 @@ describe('useAnomalyChartsInputResolver', () => { const start = moment().subtract(1, 'years'); const end = moment(); + const renderCallbacks = { + onRenderComplete: jest.fn(), + onLoading: jest.fn(), + onError: jest.fn(), + }; + beforeEach(() => { jest.useFakeTimers(); @@ -116,21 +122,27 @@ describe('useAnomalyChartsInputResolver', () => { refresh, services, 1000, - 0 + 0, + renderCallbacks ) ); expect(result.current.chartsData).toBe(undefined); expect(result.current.error).toBe(undefined); expect(result.current.isLoading).toBe(true); + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(501); + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(1); + const explorerServices = services[2]; expect(explorerServices.anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1); expect(explorerServices.anomalyExplorerService.getAnomalyData$).toHaveBeenCalledTimes(1); + expect(renderCallbacks.onRenderComplete).toHaveBeenCalledTimes(1); + embeddableInput.next({ id: 'test-explorer-charts-embeddable', jobIds: ['anotherJobId'], @@ -144,8 +156,14 @@ describe('useAnomalyChartsInputResolver', () => { }); jest.advanceTimersByTime(501); + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(2); + expect(explorerServices.anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(explorerServices.anomalyExplorerService.getAnomalyData$).toHaveBeenCalledTimes(2); + + expect(renderCallbacks.onRenderComplete).toHaveBeenCalledTimes(2); + + expect(renderCallbacks.onError).toHaveBeenCalledTimes(0); }); test.skip('should not complete the observable on error', async () => { @@ -156,7 +174,8 @@ describe('useAnomalyChartsInputResolver', () => { refresh, services, 1000, - 1 + 1, + renderCallbacks ) ); @@ -168,5 +187,6 @@ describe('useAnomalyChartsInputResolver', () => { } as Partial); expect(result.current.error).toBeDefined(); + expect(renderCallbacks.onError).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts index 8195727b2635c..c6dc3ec41ff9e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts @@ -35,7 +35,12 @@ export function useAnomalyChartsInputResolver( refresh: Observable, services: [CoreStart, MlStartDependencies, AnomalyChartsServices], chartWidth: number, - severity: number + severity: number, + renderCallbacks: { + onRenderComplete: () => void; + onLoading: () => void; + onError: (error: Error) => void; + } ): { chartsData: ExplorerChartsData | undefined; isLoading: boolean; @@ -61,6 +66,9 @@ export function useAnomalyChartsInputResolver( .pipe( tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), + tap(() => { + renderCallbacks.onLoading(); + }), switchMap(([explorerJobs, input, embeddableContainerWidth, severityValue]) => { if (!explorerJobs) { // couldn't load the list of jobs @@ -118,6 +126,8 @@ export function useAnomalyChartsInputResolver( setError(null); setChartsData(results); setIsLoading(false); + + renderCallbacks.onRenderComplete(); } }); @@ -134,5 +144,11 @@ export function useAnomalyChartsInputResolver( severity$.next(severity); }, [severity]); + useEffect(() => { + if (error) { + renderCallbacks.onError(error); + } + }, [error]); + return { chartsData, isLoading, error }; } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index e168029148006..e23869cb809b3 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -56,10 +56,28 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ); } + public onLoading() { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ loading: true, error: undefined }); + } + + public onError(error: Error) { + this.renderComplete.dispatchError(); + this.updateOutput({ loading: false, error: { name: error.name, message: error.message } }); + } + + public onRenderComplete() { + this.renderComplete.dispatchComplete(); + this.updateOutput({ loading: false, error: undefined }); + } + public render(node: HTMLElement) { super.render(node); this.node = node; + // required for the export feature to work + this.node.setAttribute('data-shared-item', ''); + const I18nContext = this.services[0].i18n.Context; const theme$ = this.services[0].theme.theme$; @@ -76,6 +94,9 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< refresh={this.reload$.asObservable()} onInputChange={this.updateInput.bind(this)} onOutputChange={this.updateOutput.bind(this)} + onRenderComplete={this.onRenderComplete.bind(this)} + onLoading={this.onLoading.bind(this)} + onError={this.onError.bind(this)} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx index 6b44073ac95bb..e9ff81ac07bdc 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx @@ -48,6 +48,9 @@ describe('ExplorerSwimlaneContainer', () => { const onInputChange = jest.fn(); const onOutputChange = jest.fn(); + const onRenderComplete = jest.fn(); + const onLoading = jest.fn(); + const onError = jest.fn(); beforeEach(() => { embeddableContext = { id: 'test-id' } as AnomalySwimlaneEmbeddable; @@ -102,6 +105,9 @@ describe('ExplorerSwimlaneContainer', () => { refresh={refresh} onInputChange={onInputChange} onOutputChange={onOutputChange} + onLoading={onLoading} + onRenderComplete={onRenderComplete} + onError={onError} />, defaultOptions ); @@ -141,6 +147,9 @@ describe('ExplorerSwimlaneContainer', () => { refresh={refresh} onInputChange={onInputChange} onOutputChange={onOutputChange} + onLoading={onLoading} + onRenderComplete={onRenderComplete} + onError={onError} />, defaultOptions ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 28598974ba4d0..ac9586bfa69ae 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -35,6 +35,9 @@ export interface ExplorerSwimlaneContainerProps { refresh: Observable; onInputChange: (input: Partial) => void; onOutputChange: (output: Partial) => void; + onRenderComplete: () => void; + onLoading: () => void; + onError: (error: Error) => void; } export const EmbeddableSwimLaneContainer: FC = ({ @@ -45,6 +48,9 @@ export const EmbeddableSwimLaneContainer: FC = ( refresh, onInputChange, onOutputChange, + onRenderComplete, + onLoading, + onError, }) => { const [chartWidth, setChartWidth] = useState(0); @@ -61,7 +67,8 @@ export const EmbeddableSwimLaneContainer: FC = ( refresh, services, chartWidth, - fromPage + fromPage, + { onRenderComplete, onError, onLoading } ); useEffect(() => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index 28aae4bcc0a55..de2281b395000 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -19,6 +19,12 @@ describe('useSwimlaneInputResolver', () => { let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices]; let onInputChange: jest.Mock; + const renderCallbacks = { + onRenderComplete: jest.fn(), + onLoading: jest.fn(), + onError: jest.fn(), + }; + beforeEach(() => { jest.useFakeTimers(); @@ -78,6 +84,7 @@ describe('useSwimlaneInputResolver', () => { ]; onInputChange = jest.fn(); }); + afterEach(() => { jest.useRealTimers(); jest.clearAllMocks(); @@ -91,7 +98,8 @@ describe('useSwimlaneInputResolver', () => { refresh, services, 1000, - 1 + 1, + renderCallbacks ) ); @@ -106,6 +114,9 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(1); + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(1); + expect(renderCallbacks.onRenderComplete).toHaveBeenCalledTimes(1); + await act(async () => { embeddableInput.next({ id: 'test-swimlane-embeddable', @@ -121,6 +132,9 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(2); + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(2); + expect(renderCallbacks.onRenderComplete).toHaveBeenCalledTimes(2); + await act(async () => { embeddableInput.next({ id: 'test-swimlane-embeddable', @@ -135,6 +149,9 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3); + + expect(renderCallbacks.onLoading).toHaveBeenCalledTimes(3); + expect(renderCallbacks.onRenderComplete).toHaveBeenCalledTimes(3); }); test('should not complete the observable on error', async () => { @@ -145,7 +162,8 @@ describe('useSwimlaneInputResolver', () => { refresh, services, 1000, - 1 + 1, + renderCallbacks ) ); @@ -160,5 +178,7 @@ describe('useSwimlaneInputResolver', () => { }); expect(result.current[6]?.message).toBe('Invalid job'); + + expect(renderCallbacks.onError).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 8b0c89bbd16b7..ee3a635071071 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -46,10 +46,15 @@ const FETCH_RESULTS_DEBOUNCE_MS = 500; export function useSwimlaneInputResolver( embeddableInput$: Observable, onInputChange: (output: Partial) => void, - refresh: Observable, + refresh: Observable, services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices], chartWidth: number, - fromPage: number + fromPage: number, + renderCallbacks: { + onRenderComplete: () => void; + onLoading: () => void; + onError: (error: Error) => void; + } ): [ string | undefined, OverallSwimlaneData | undefined, @@ -122,6 +127,9 @@ export function useSwimlaneInputResolver( .pipe( tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), + tap(() => { + renderCallbacks.onLoading(); + }), switchMap(([explorerJobs, input, bucketInterval, fromPageInput, perPageFromState]) => { if (!explorerJobs) { // couldn't load the list of jobs @@ -227,6 +235,18 @@ export function useSwimlaneInputResolver( chartWidth$.next(chartWidth); }, [chartWidth]); + useEffect(() => { + if (error) { + renderCallbacks.onError(error); + } + }, [error]); + + useEffect(() => { + if (swimlaneData) { + renderCallbacks.onRenderComplete(); + } + }, [swimlaneData]); + return [ swimlaneType, swimlaneData,