fix(slo): Handle partial indicator url state (#167247)

This commit is contained in:
Kevin Delemme 2023-09-29 10:19:28 -04:00 committed by GitHub
parent 8c17d8ab5d
commit 9d3213e137
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 246 additions and 43 deletions

View file

@ -7,8 +7,9 @@
import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React from 'react'; import React, { useEffect } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices';
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
import { CreateSLOForm } from '../../types'; import { CreateSLOForm } from '../../types';
import { FieldSelector } from '../apm_common/field_selector'; import { FieldSelector } from '../apm_common/field_selector';
@ -17,10 +18,17 @@ import { IndexFieldSelector } from '../common/index_field_selector';
import { QueryBuilder } from '../common/query_builder'; import { QueryBuilder } from '../common/query_builder';
export function ApmAvailabilityIndicatorTypeForm() { export function ApmAvailabilityIndicatorTypeForm() {
const { watch } = useFormContext<CreateSLOForm>(); const { watch, setValue } = useFormContext<CreateSLOForm>();
const index = watch('indicator.params.index'); const { data: apmIndex } = useFetchApmIndex();
useEffect(() => {
if (apmIndex !== '') {
setValue('indicator.params.index', apmIndex);
}
}, [setValue, apmIndex]);
const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = const { isLoading: isIndexFieldsLoading, data: indexFields = [] } =
useFetchIndexPatternFields(index); useFetchIndexPatternFields(apmIndex);
const partitionByFields = indexFields.filter((field) => field.aggregatable); const partitionByFields = indexFields.filter((field) => field.aggregatable);
return ( return (
@ -144,8 +152,8 @@ export function ApmAvailabilityIndicatorTypeForm() {
placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', {
defaultMessage: 'Select an optional field to partition by', defaultMessage: 'Select an optional field to partition by',
})} })}
isLoading={!!index && isIndexFieldsLoading} isLoading={!!apmIndex && isIndexFieldsLoading}
isDisabled={!index} isDisabled={!apmIndex}
/> />
<DataPreviewChart /> <DataPreviewChart />

View file

@ -7,8 +7,9 @@
import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React from 'react'; import React, { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices';
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
import { CreateSLOForm } from '../../types'; import { CreateSLOForm } from '../../types';
import { FieldSelector } from '../apm_common/field_selector'; import { FieldSelector } from '../apm_common/field_selector';
@ -17,10 +18,17 @@ import { IndexFieldSelector } from '../common/index_field_selector';
import { QueryBuilder } from '../common/query_builder'; import { QueryBuilder } from '../common/query_builder';
export function ApmLatencyIndicatorTypeForm() { export function ApmLatencyIndicatorTypeForm() {
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>(); const { control, watch, getFieldState, setValue } = useFormContext<CreateSLOForm>();
const index = watch('indicator.params.index'); const { data: apmIndex } = useFetchApmIndex();
useEffect(() => {
if (apmIndex !== '') {
setValue('indicator.params.index', apmIndex);
}
}, [setValue, apmIndex]);
const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = const { isLoading: isIndexFieldsLoading, data: indexFields = [] } =
useFetchIndexPatternFields(index); useFetchIndexPatternFields(apmIndex);
const partitionByFields = indexFields.filter((field) => field.aggregatable); const partitionByFields = indexFields.filter((field) => field.aggregatable);
return ( return (
@ -187,8 +195,8 @@ export function ApmLatencyIndicatorTypeForm() {
placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { placeholder={i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', {
defaultMessage: 'Select an optional field to partition by', defaultMessage: 'Select an optional field to partition by',
})} })}
isLoading={!!index && isIndexFieldsLoading} isLoading={!!apmIndex && isIndexFieldsLoading}
isDisabled={!index} isDisabled={!apmIndex}
/> />
<DataPreviewChart /> <DataPreviewChart />

View file

@ -62,7 +62,7 @@ export function SloEditForm({ slo }: Props) {
sloIds: slo?.id ? [slo.id] : undefined, sloIds: slo?.id ? [slo.id] : undefined,
}); });
const sloFormValuesUrlState = useParseUrlState(); const sloFormValuesFromUrlState = useParseUrlState();
const isAddRuleFlyoutOpen = useAddRuleFlyoutState(isEditMode); const isAddRuleFlyoutOpen = useAddRuleFlyoutState(isEditMode);
const [isCreateRuleCheckboxChecked, setIsCreateRuleCheckboxChecked] = useState(true); const [isCreateRuleCheckboxChecked, setIsCreateRuleCheckboxChecked] = useState(true);
@ -73,7 +73,7 @@ export function SloEditForm({ slo }: Props) {
}, [isEditMode, rules, slo]); }, [isEditMode, rules, slo]);
const methods = useForm<CreateSLOForm>({ const methods = useForm<CreateSLOForm>({
defaultValues: Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, sloFormValuesUrlState), defaultValues: Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, sloFormValuesFromUrlState),
values: transformSloResponseToCreateSloForm(slo), values: transformSloResponseToCreateSloForm(slo),
mode: 'all', mode: 'all',
}); });

View file

@ -0,0 +1,152 @@
/*
* 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 { transformPartialUrlStateToFormState as transform } from './process_slo_form_values';
describe('Transform Partial URL State into partial State Form', () => {
describe('indicators', () => {
it("returns an empty '{}' when no indicator type is specified", () => {
expect(transform({ indicator: { params: { index: 'my-index' } } })).toEqual({});
});
it('handles partial APM Availability state', () => {
expect(
transform({
indicator: {
type: 'sli.apm.transactionErrorRate',
params: {
service: 'override-service',
},
},
})
).toEqual({
indicator: {
type: 'sli.apm.transactionErrorRate',
params: {
service: 'override-service',
environment: '',
filter: '',
index: '',
transactionName: '',
transactionType: '',
},
},
});
});
it('handles partial APM Latency state', () => {
expect(
transform({
indicator: {
type: 'sli.apm.transactionDuration',
params: {
service: 'override-service',
},
},
})
).toEqual({
indicator: {
type: 'sli.apm.transactionDuration',
params: {
service: 'override-service',
environment: '',
filter: '',
index: '',
transactionName: '',
transactionType: '',
threshold: 250,
},
},
});
});
it('handles partial Custom KQL state', () => {
expect(
transform({
indicator: {
type: 'sli.kql.custom',
params: {
good: "some.override.filter:'foo'",
index: 'override-index',
},
},
})
).toEqual({
indicator: {
type: 'sli.kql.custom',
params: {
index: 'override-index',
timestampField: '',
filter: '',
good: "some.override.filter:'foo'",
total: '',
},
},
});
});
it('handles partial Custom Metric state', () => {
expect(
transform({
indicator: {
type: 'sli.metric.custom',
params: {
index: 'override-index',
},
},
})
).toEqual({
indicator: {
type: 'sli.metric.custom',
params: {
index: 'override-index',
filter: '',
timestampField: '',
good: {
equation: 'A',
metrics: [{ aggregation: 'sum', field: '', name: 'A' }],
},
total: {
equation: 'A',
metrics: [{ aggregation: 'sum', field: '', name: 'A' }],
},
},
},
});
});
it('handles partial Custom Histogram state', () => {
expect(
transform({
indicator: {
type: 'sli.histogram.custom',
params: {
index: 'override-index',
},
},
})
).toEqual({
indicator: {
type: 'sli.histogram.custom',
params: {
index: 'override-index',
filter: '',
timestampField: '',
good: {
aggregation: 'value_count',
field: '',
},
total: {
aggregation: 'value_count',
field: '',
},
},
},
});
});
});
});

View file

@ -5,8 +5,17 @@
* 2.0. * 2.0.
*/ */
import { CreateSLOInput, SLOWithSummaryResponse, UpdateSLOInput } from '@kbn/slo-schema'; import { CreateSLOInput, Indicator, SLOWithSummaryResponse, UpdateSLOInput } from '@kbn/slo-schema';
import { assertNever } from '@kbn/std';
import { RecursivePartial } from '@kbn/utility-types';
import { toDuration } from '../../../utils/slo/duration'; import { toDuration } from '../../../utils/slo/duration';
import {
APM_AVAILABILITY_DEFAULT_VALUES,
APM_LATENCY_DEFAULT_VALUES,
CUSTOM_KQL_DEFAULT_VALUES,
CUSTOM_METRIC_DEFAULT_VALUES,
HISTOGRAM_DEFAULT_VALUES,
} from '../constants';
import { CreateSLOForm } from '../types'; import { CreateSLOForm } from '../types';
export function transformSloResponseToCreateSloForm( export function transformSloResponseToCreateSloForm(
@ -91,21 +100,50 @@ export function transformValuesToUpdateSLOInput(values: CreateSLOForm): UpdateSL
}; };
} }
export function transformPartialCreateSLOInputToPartialCreateSLOForm( function transformPartialIndicatorState(
values: Partial<CreateSLOInput> indicator?: RecursivePartial<Indicator>
): Partial<CreateSLOForm> { ): Indicator | undefined {
return { if (indicator === undefined || indicator.type === undefined) return undefined;
...values,
...(values.objective && { const indicatorType = indicator.type;
objective: { switch (indicatorType) {
target: values.objective.target * 100, case 'sli.apm.transactionDuration':
...(values.objective.timesliceTarget && { return {
timesliceTarget: values.objective.timesliceTarget * 100, type: 'sli.apm.transactionDuration' as const,
}), params: Object.assign({}, APM_LATENCY_DEFAULT_VALUES.params, indicator.params ?? {}),
...(values.objective.timesliceWindow && { };
timesliceWindow: String(toDuration(values.objective.timesliceWindow).value), case 'sli.apm.transactionErrorRate':
}), return {
}, type: 'sli.apm.transactionErrorRate' as const,
}), params: Object.assign({}, APM_AVAILABILITY_DEFAULT_VALUES.params, indicator.params ?? {}),
}; };
case 'sli.histogram.custom':
return {
type: 'sli.histogram.custom' as const,
params: Object.assign({}, HISTOGRAM_DEFAULT_VALUES.params, indicator.params ?? {}),
};
case 'sli.kql.custom':
return {
type: 'sli.kql.custom' as const,
params: Object.assign({}, CUSTOM_KQL_DEFAULT_VALUES.params, indicator.params ?? {}),
};
case 'sli.metric.custom':
return {
type: 'sli.metric.custom' as const,
params: Object.assign({}, CUSTOM_METRIC_DEFAULT_VALUES.params, indicator.params ?? {}),
};
default:
assertNever(indicatorType);
}
}
export function transformPartialUrlStateToFormState(
values: RecursivePartial<Pick<CreateSLOInput, 'indicator'>>
): Partial<CreateSLOForm> | {} {
const state: Partial<CreateSLOForm> = {};
const parsedIndicator = transformPartialIndicatorState(values.indicator);
if (parsedIndicator !== undefined) state.indicator = parsedIndicator;
return state;
} }

View file

@ -7,8 +7,9 @@
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { CreateSLOInput } from '@kbn/slo-schema'; import { CreateSLOInput } from '@kbn/slo-schema';
import { RecursivePartial } from '@kbn/utility-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { transformPartialCreateSLOInputToPartialCreateSLOForm } from '../helpers/process_slo_form_values'; import { transformPartialUrlStateToFormState } from '../helpers/process_slo_form_values';
import { CreateSLOForm } from '../types'; import { CreateSLOForm } from '../types';
export function useParseUrlState(): Partial<CreateSLOForm> | null { export function useParseUrlState(): Partial<CreateSLOForm> | null {
@ -19,7 +20,7 @@ export function useParseUrlState(): Partial<CreateSLOForm> | null {
useHashQuery: false, useHashQuery: false,
}); });
const urlParams = urlStateStorage.get<Partial<CreateSLOInput>>('_a'); const urlParams = urlStateStorage.get<RecursivePartial<CreateSLOInput>>('_a');
return !!urlParams ? transformPartialCreateSLOInputToPartialCreateSLOForm(urlParams) : null; return !!urlParams ? transformPartialUrlStateToFormState(urlParams) : null;
} }

View file

@ -32,8 +32,8 @@ export function useUnregisterFields({ isEditMode }: { isEditMode: boolean }) {
const [indicatorTypeState, setIndicatorTypeState] = useState<IndicatorType>( const [indicatorTypeState, setIndicatorTypeState] = useState<IndicatorType>(
watch('indicator.type') watch('indicator.type')
); );
const indicatorType = watch('indicator.type');
const indicatorType = watch('indicator.type');
useEffect(() => { useEffect(() => {
if (indicatorType !== indicatorTypeState && !isEditMode) { if (indicatorType !== indicatorTypeState && !isEditMode) {
setIndicatorTypeState(indicatorType); setIndicatorTypeState(indicatorType);

View file

@ -370,7 +370,7 @@ describe('SLO Edit Page', () => {
const history = createBrowserHistory(); const history = createBrowserHistory();
history.push( history.push(
'/slos/create?_a=(name:%27prefilledSloName%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))' '/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))'
); );
jest.spyOn(Router, 'useHistory').mockReturnValue(history); jest.spyOn(Router, 'useHistory').mockReturnValue(history);
jest jest
@ -409,18 +409,14 @@ describe('SLO Edit Page', () => {
expect(screen.queryByTestId('sloForm')).toBeTruthy(); expect(screen.queryByTestId('sloForm')).toBeTruthy();
expect(screen.queryByTestId('sloEditFormIndicatorSection')).toBeTruthy(); expect(screen.queryByTestId('sloEditFormIndicatorSection')).toBeTruthy();
// Show default values from the kql indicator
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue( expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
'sli.apm.transactionDuration' 'sli.apm.transactionDuration'
); );
expect(screen.queryByTestId('sloEditFormObjectiveSection')).toBeTruthy();
expect(screen.queryByTestId('sloEditFormDescriptionSection')).toBeTruthy();
expect(screen.queryByTestId('apmLatencyServiceSelector')).toHaveTextContent('cartService'); expect(screen.queryByTestId('apmLatencyServiceSelector')).toHaveTextContent('cartService');
expect(screen.queryByTestId('apmLatencyEnvironmentSelector')).toHaveTextContent('prod'); expect(screen.queryByTestId('apmLatencyEnvironmentSelector')).toHaveTextContent('prod');
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue('prefilledSloName'); expect(screen.queryByTestId('sloEditFormObjectiveSection')).toBeFalsy();
expect(screen.queryByTestId('sloEditFormDescriptionSection')).toBeFalsy();
}); });
}); });