mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
fix(slo): Handle partial indicator url state (#167247)
This commit is contained in:
parent
8c17d8ab5d
commit
9d3213e137
8 changed files with 246 additions and 43 deletions
|
@ -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 />
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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',
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue