feat(slo): add APM availability indicator type form (#151379)

This commit is contained in:
Kevin Delemme 2023-02-16 08:30:30 -05:00 committed by GitHub
parent b76ea2f69f
commit a5113d8001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 252 additions and 19 deletions

3
.github/CODEOWNERS vendored
View file

@ -770,6 +770,9 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations
/x-pack/plugins/observability/public/pages/cases @elastic/actionable-observability
/x-pack/plugins/observability/public/pages/rules @elastic/actionable-observability
/x-pack/plugins/observability/public/pages/rule_details @elastic/actionable-observability
/x-pack/plugins/observability/public/pages/slos @elastic/actionable-observability
/x-pack/plugins/observability/public/pages/slo_edit @elastic/actionable-observability
/x-pack/plugins/observability/public/pages/slo_details @elastic/actionable-observability
/x-pack/test/observability_functional @elastic/actionable-observability
# keep it below actionable observability

View file

@ -0,0 +1,37 @@
/*
* 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 from 'react';
import { ComponentStory } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { KibanaReactStorybookDecorator } from '../../../../utils/kibana_react.storybook_decorator';
import {
ApmAvailabilityIndicatorTypeForm as Component,
Props,
} from './apm_availability_indicator_type_form';
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
export default {
component: Component,
title: 'app/SLO/EditPage/ApmAvailability/Form',
decorators: [KibanaReactStorybookDecorator],
};
const Template: ComponentStory<typeof Component> = (props: Props) => {
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
return (
<FormProvider {...methods}>
<Component {...props} control={methods.control} />
</FormProvider>
);
};
const defaultProps = {};
export const Form = Template.bind({});
Form.args = defaultProps;

View file

@ -0,0 +1,157 @@
/*
* 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 from 'react';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
} from '@elastic/eui';
import { Control, Controller } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import type { CreateSLOInput } from '@kbn/slo-schema';
import { FieldSelector } from '../common/field_selector';
export interface Props {
control: Control<CreateSLOInput>;
}
export function ApmAvailabilityIndicatorTypeForm({ control }: Props) {
return (
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="row" gutterSize="l">
<FieldSelector
allowAllOption={false}
label={i18n.translate('xpack.observability.slos.sloEdit.apmAvailability.serviceName', {
defaultMessage: 'Service name',
})}
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.serviceName.placeholder',
{
defaultMessage: 'Select the APM service',
}
)}
fieldName="service.name"
name="indicator.params.service"
control={control}
dataTestSubj="apmAvailabilityServiceSelector"
/>
<FieldSelector
label={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.serviceEnvironment',
{
defaultMessage: 'Service environment',
}
)}
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.serviceEnvironment.placeholder',
{
defaultMessage: 'Select the environment',
}
)}
fieldName="service.environment"
name="indicator.params.environment"
control={control}
dataTestSubj="apmAvailabilityEnvironmentSelector"
/>
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="l">
<FieldSelector
label={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.transactionType',
{
defaultMessage: 'Transaction type',
}
)}
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.transactionType.placeholder',
{
defaultMessage: 'Select the transaction type',
}
)}
fieldName="transaction.type"
name="indicator.params.transactionType"
control={control}
dataTestSubj="apmAvailabilityTransactionTypeSelector"
/>
<FieldSelector
label={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.transactionName',
{
defaultMessage: 'Transaction name',
}
)}
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.transactionName.placeholder',
{
defaultMessage: 'Select the transaction name',
}
)}
fieldName="transaction.name"
name="indicator.params.transactionName"
control={control}
dataTestSubj="apmAvailabilityTransactionNameSelector"
/>
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="l">
<EuiFlexItem>
<EuiFormLabel>
{i18n.translate('xpack.observability.slos.sloEdit.apmAvailability.goodStatusCodes', {
defaultMessage: 'Good status codes',
})}
</EuiFormLabel>
<Controller
shouldUnregister={true}
name="indicator.params.goodStatusCodes"
control={control}
defaultValue={['2xx', '3xx', '4xx']}
rules={{ required: true }}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiComboBox
{...field}
aria-label={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.goodStatusCodes.placeholder',
{
defaultMessage: 'Select the good status codes',
}
)}
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.apmAvailability.goodStatusCodes.placeholder',
{
defaultMessage: 'Select the good status codes',
}
)}
isInvalid={!!fieldState.error}
options={generateStatusCodeOptions()}
selectedOptions={generateStatusCodeOptions(field.value)}
onChange={(selected: EuiComboBoxOptionOption[]) => {
field.onChange(selected.map((opts) => opts.value));
}}
isClearable={true}
data-test-subj="sloEditApmAvailabilityGoodStatusCodesSelector"
/>
)}
/>
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
</EuiFlexGroup>
);
}
function generateStatusCodeOptions(codes: string[] = ['2xx', '3xx', '4xx', '5xx']) {
return codes.map((code) => ({
label: code,
value: code,
'data-test-subj': `${code}Option`,
}));
}

View file

@ -11,7 +11,7 @@ import { Control, Controller } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import type { CreateSLOInput } from '@kbn/slo-schema';
import { FieldSelector } from './field_selector';
import { FieldSelector } from '../common/field_selector';
export interface Props {
control: Control<CreateSLOInput>;

View file

@ -15,7 +15,7 @@ import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
export default {
component: Component,
title: 'app/SLO/EditPage/ApmLatency/FieldSelector',
title: 'app/SLO/EditPage/Common/FieldSelector',
decorators: [KibanaReactStorybookDecorator],
};

View file

@ -52,7 +52,7 @@ export function FieldSelector({
? [
{
value: '*',
label: i18n.translate('xpack.observability.slos.sloEdit.apmFieldSelector.all', {
label: i18n.translate('xpack.observability.slos.sloEdit.fieldSelector.all', {
defaultMessage: 'All',
}),
},

View file

@ -9,6 +9,7 @@ import React, { useEffect } from 'react';
import {
EuiAvatar,
EuiButton,
EuiFlexGroup,
EuiFormLabel,
EuiPanel,
EuiSelect,
@ -36,6 +37,7 @@ import {
import { paths } from '../../../config';
import { SLI_OPTIONS, SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
import { ApmLatencyIndicatorTypeForm } from './apm_latency/apm_latency_indicator_type_form';
import { ApmAvailabilityIndicatorTypeForm } from './apm_availability/apm_availability_indicator_type_form';
export interface Props {
slo: SLOWithSummaryResponse | undefined;
@ -110,6 +112,8 @@ export function SloEditForm({ slo }: Props) {
return <CustomKqlIndicatorTypeForm control={control} watch={watch} />;
case 'sli.apm.transactionDuration':
return <ApmLatencyIndicatorTypeForm control={control} />;
case 'sli.apm.transactionErrorRate':
return <ApmAvailabilityIndicatorTypeForm control={control} />;
default:
return null;
}
@ -225,22 +229,34 @@ export function SloEditForm({ slo }: Props) {
<EuiSpacer size="xl" />
<EuiButton
fill
color="primary"
data-test-subj="sloFormSubmitButton"
onClick={handleSubmit}
disabled={!formState.isValid}
isLoading={loading && !error}
>
{isEditMode
? i18n.translate('xpack.observability.slos.sloEdit.editSloButton', {
defaultMessage: 'Update SLO',
})
: i18n.translate('xpack.observability.slos.sloEdit.createSloButton', {
defaultMessage: 'Create SLO',
})}
</EuiButton>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiButton
fill
color="primary"
data-test-subj="sloFormSubmitButton"
onClick={handleSubmit}
disabled={!formState.isValid}
isLoading={loading && !error}
>
{isEditMode
? i18n.translate('xpack.observability.slos.sloEdit.editSloButton', {
defaultMessage: 'Update SLO',
})
: i18n.translate('xpack.observability.slos.sloEdit.createSloButton', {
defaultMessage: 'Create SLO',
})}
</EuiButton>
<EuiButton
fill
color="ghost"
data-test-subj="sloFormCancelButton"
onClick={() => navigateToUrl(basePath.prepend(paths.observability.slos))}
>
{i18n.translate('xpack.observability.slos.sloEdit.cancelButton', {
defaultMessage: 'Cancel',
})}
</EuiButton>
</EuiFlexGroup>
<EuiSpacer size="xl" />
</EuiPanel>

View file

@ -24,6 +24,12 @@ export const SLI_OPTIONS: Array<{
defaultMessage: 'APM latency',
}),
},
{
value: 'sli.apm.transactionErrorRate',
text: i18n.translate('xpack.observability.slos.sliTypes.apmAvailabilityIndicator', {
defaultMessage: 'APM availability',
}),
},
];
export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: string }> = [

View file

@ -45,6 +45,20 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
(field) => !getFieldState(field, formState).invalid
);
break;
case 'sli.apm.transactionErrorRate':
isIndicatorSectionValid =
(
[
'indicator.params.service',
'indicator.params.environment',
'indicator.params.transactionType',
'indicator.params.transactionName',
] as const
).every((field) => !getFieldState(field, formState).invalid && getValues(field) !== '') &&
(['indicator.params.index', 'indicator.params.goodStatusCodes'] as const).every(
(field) => !getFieldState(field, formState).invalid
);
break;
default:
isIndicatorSectionValid = false;
break;