mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
chore(slo): improve error handling (#160081)
Resolves https://github.com/elastic/kibana/issues/156145 Resolves https://github.com/elastic/kibana/issues/160293 ## 📝 Summary This PR fixes a bug related to `shouldUnregister` used on controlled fields which removes part of the form state while submitting the form, due to the components unmounting. This is a weird issue between React Hook Form and React Query, as if we were not using optimistic update with RQ, we won't notice the problem in the first place. I have done a few things in this PR: 1. I've introduced a `CreateSLOForm` type to decouple what the API is expecting (CreateSLOInput) and how we structure the form and store the form state. This is particularly useful when building a partial `CreateSLOForm` from a partial url state `_a`. 2. I've introduced a hook that handles the change of indicator correctly, resetting the default value as we change. The default values are typed with each indicator schema type, and the hook will throw when a new indicator is introduced but not handled there. 3. I've removed the custom metric manual useEffect and instead rely on hidden registered inputs. 4. The time window type handles correctly the initial state, and reload the default values when we change from rolling <-> calendar aligned. 5. I've grouped some code from the main form component into their own hook to 1. add a layer of abstraction and 2. make the code more cohesive 6. When the create SLO call fails, we redirect the user to the form with the previously entered values. ## 🧪 Testing I would suggest to create and edit some SLOs, playing with the different time window, budgeting method, indicators. The main thing to look for are: 1. Switching indicator reset the form as expected 2. When editing an SLO, all the form fields are populated correctly with the initial SLO values. 3. Creating an SLO with a bad indicator, for example with an invalid KQL query, will redirect to the form with the previous value filled. https://www.loom.com/share/516c3d5a1fa74db6bf839cfa0cf41f5d?sid=f0a1a33f-eb29-4b8f-b65f-ffce2313bad8 --------- Co-authored-by: Coen Warmer <coen.warmer@gmail.com>
This commit is contained in:
parent
78cec76d24
commit
f5a111eaab
30 changed files with 553 additions and 316 deletions
|
@ -12,6 +12,7 @@ import {
|
|||
historicalSummarySchema,
|
||||
indicatorSchema,
|
||||
indicatorTypesArraySchema,
|
||||
indicatorTypesSchema,
|
||||
kqlCustomIndicatorSchema,
|
||||
metricCustomIndicatorSchema,
|
||||
objectiveSchema,
|
||||
|
@ -189,17 +190,15 @@ type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryRe
|
|||
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
|
||||
|
||||
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
|
||||
type GetPreviewDataResponse = t.TypeOf<typeof getPreviewDataResponseSchema>;
|
||||
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
|
||||
|
||||
type APMTransactionErrorRateIndicatorSchema = t.TypeOf<
|
||||
typeof apmTransactionErrorRateIndicatorSchema
|
||||
>;
|
||||
type APMTransactionDurationIndicatorSchema = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
|
||||
type GetSLOBurnRatesResponse = t.OutputOf<typeof getSLOBurnRatesResponseSchema>;
|
||||
type BudgetingMethod = t.TypeOf<typeof budgetingMethodSchema>;
|
||||
type TimeWindow = t.TypeOf<typeof timeWindowTypeSchema>;
|
||||
|
||||
type BudgetingMethod = t.OutputOf<typeof budgetingMethodSchema>;
|
||||
type TimeWindow = t.OutputOf<typeof timeWindowTypeSchema>;
|
||||
type IndicatorType = t.OutputOf<typeof indicatorTypesSchema>;
|
||||
type Indicator = t.OutputOf<typeof indicatorSchema>;
|
||||
type APMTransactionErrorRateIndicator = t.OutputOf<typeof apmTransactionErrorRateIndicatorSchema>;
|
||||
type APMTransactionDurationIndicator = t.OutputOf<typeof apmTransactionDurationIndicatorSchema>;
|
||||
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
|
||||
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;
|
||||
|
||||
|
@ -242,9 +241,10 @@ export type {
|
|||
UpdateSLOInput,
|
||||
UpdateSLOParams,
|
||||
UpdateSLOResponse,
|
||||
APMTransactionDurationIndicatorSchema,
|
||||
APMTransactionErrorRateIndicatorSchema,
|
||||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
GetSLOBurnRatesResponse,
|
||||
IndicatorType,
|
||||
Indicator,
|
||||
MetricCustomIndicator,
|
||||
KQLCustomIndicator,
|
||||
|
|
|
@ -20,6 +20,8 @@ export const paths = {
|
|||
slos: SLOS_PAGE_LINK,
|
||||
slosWelcome: `${SLOS_PAGE_LINK}/welcome`,
|
||||
sloCreate: `${SLOS_PAGE_LINK}/create`,
|
||||
sloCreateWithEncodedForm: (encodedParams: string) =>
|
||||
`${SLOS_PAGE_LINK}/create?_a=${encodedParams}`,
|
||||
sloEdit: (sloId: string) => `${SLOS_PAGE_LINK}/edit/${encodeURI(sloId)}`,
|
||||
sloDetails: (sloId: string) => `${SLOS_PAGE_LINK}/${encodeURI(sloId)}`,
|
||||
},
|
||||
|
|
|
@ -54,13 +54,6 @@ export function useCloneSlo() {
|
|||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(queryKey ?? sloKeys.lists(), optimisticUpdate);
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.clone.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
|
@ -76,7 +69,13 @@ export function useCloneSlo() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { slo }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.clone.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(sloKeys.lists());
|
||||
},
|
||||
}
|
||||
|
|
|
@ -5,16 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { encode } from '@kbn/rison';
|
||||
import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
import { paths } from '../../config/paths';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
|
||||
export function useCreateSlo() {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
@ -27,23 +30,6 @@ export function useCreateSlo() {
|
|||
},
|
||||
{
|
||||
mutationKey: ['createSlo'],
|
||||
onSuccess: (_data, { slo: { name } }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.create.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name },
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(sloKeys.lists());
|
||||
},
|
||||
onError: (error, { slo: { name } }) => {
|
||||
toasts.addError(new Error(String(error)), {
|
||||
title: i18n.translate('xpack.observability.slo.create.errorNotification', {
|
||||
defaultMessage: 'Something went wrong while creating {name}',
|
||||
values: { name },
|
||||
}),
|
||||
});
|
||||
},
|
||||
onMutate: async ({ slo }) => {
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||
await queryClient.cancelQueries(sloKeys.lists());
|
||||
|
@ -66,6 +52,27 @@ export function useCreateSlo() {
|
|||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
onSuccess: (_data, { slo }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.create.successNotification', {
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(sloKeys.lists());
|
||||
},
|
||||
onError: (error, { slo }) => {
|
||||
toasts.addError(new Error(String(error)), {
|
||||
title: i18n.translate('xpack.observability.slo.create.errorNotification', {
|
||||
defaultMessage: 'Something went wrong while creating {name}',
|
||||
values: { name: slo.name },
|
||||
}),
|
||||
});
|
||||
|
||||
navigateToUrl(
|
||||
http.basePath.prepend(paths.observability.sloCreateWithEncodedForm(encode(slo)))
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,18 +51,11 @@ export function useDeleteSlo() {
|
|||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(queryKey ?? sloKeys.lists(), optimisticUpdate);
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.slo.delete.successNotification', {
|
||||
defaultMessage: 'Deleted {name}',
|
||||
values: { name: slo.name },
|
||||
})
|
||||
);
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousSloList: data };
|
||||
},
|
||||
// If the mutation fails, use the context returned from onMutate to roll back
|
||||
onError: (_err, slo, context) => {
|
||||
onError: (_err, { name }, context) => {
|
||||
if (context?.previousSloList) {
|
||||
queryClient.setQueryData(sloKeys.lists(), context.previousSloList);
|
||||
}
|
||||
|
@ -70,11 +63,17 @@ export function useDeleteSlo() {
|
|||
toasts.addDanger(
|
||||
i18n.translate('xpack.observability.slo.slo.delete.errorNotification', {
|
||||
defaultMessage: 'Failed to delete {name}',
|
||||
values: { name: slo.name },
|
||||
values: { name },
|
||||
})
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { name }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.observability.slo.slo.delete.successNotification', {
|
||||
defaultMessage: 'Deleted {name}',
|
||||
values: { name },
|
||||
})
|
||||
);
|
||||
if (
|
||||
// @ts-ignore
|
||||
queryClient.getQueryCache().find(sloKeys.lists())?.options.refetchInterval === undefined
|
||||
|
|
|
@ -20,8 +20,8 @@ import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
|||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
import { paths } from '../../../config/paths';
|
||||
import {
|
||||
transformSloResponseToCreateSloInput,
|
||||
transformValuesToCreateSLOInput,
|
||||
transformSloResponseToCreateSloForm,
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SloDeleteConfirmationModal } from '../../slos/components/slo_delete_confirmation_modal';
|
||||
import type { RulesParams } from '../../../locators/rules';
|
||||
|
@ -111,8 +111,8 @@ export function HeaderControl({ isLoading, slo }: Props) {
|
|||
if (slo) {
|
||||
setIsPopoverOpen(false);
|
||||
|
||||
const newSlo = transformValuesToCreateSLOInput(
|
||||
transformSloResponseToCreateSloInput({ ...slo, name: `[Copy] ${slo.name}` })!
|
||||
const newSlo = transformCreateSLOFormToCreateSLOInput(
|
||||
transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })!
|
||||
);
|
||||
|
||||
cloneSlo({ slo: newSlo, originalSloId: slo.id });
|
||||
|
|
|
@ -234,7 +234,7 @@ describe('SLO Details Page', () => {
|
|||
|
||||
fireEvent.click(button!);
|
||||
|
||||
const { id, createdAt, enabled, revision, summary, updatedAt, ...newSlo } = slo;
|
||||
const { id, createdAt, enabled, revision, summary, settings, updatedAt, ...newSlo } = slo;
|
||||
|
||||
expect(mockClone).toBeCalledWith({
|
||||
originalSloId: slo.id,
|
||||
|
|
|
@ -5,22 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { FieldSelector } from '../apm_common/field_selector';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
|
||||
export function ApmAvailabilityIndicatorTypeForm() {
|
||||
const { setValue, watch } = useFormContext<CreateSLOInput>();
|
||||
const { data: apmIndex } = useFetchApmIndex();
|
||||
useEffect(() => {
|
||||
setValue('indicator.params.index', apmIndex);
|
||||
}, [apmIndex, setValue]);
|
||||
const { watch } = useFormContext<CreateSLOForm>();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
|
|
|
@ -5,17 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
Suggestion,
|
||||
useFetchApmSuggestions,
|
||||
} from '../../../../hooks/slo/use_fetch_apm_suggestions';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
|
@ -27,7 +26,7 @@ export interface Props {
|
|||
dataTestSubj: string;
|
||||
fieldName: string;
|
||||
label: string;
|
||||
name: FieldPath<CreateSLOInput>;
|
||||
name: FieldPath<CreateSLOForm>;
|
||||
placeholder: string;
|
||||
tooltip?: ReactNode;
|
||||
}
|
||||
|
@ -41,7 +40,7 @@ export function FieldSelector({
|
|||
placeholder,
|
||||
tooltip,
|
||||
}: Props) {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
const serviceName = watch('indicator.params.service');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const { suggestions, isLoading } = useFetchApmSuggestions({
|
||||
|
@ -80,7 +79,6 @@ export function FieldSelector({
|
|||
isInvalid={getFieldState(name).invalid}
|
||||
>
|
||||
<Controller
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
name={name}
|
||||
control={control}
|
||||
|
|
|
@ -5,22 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { FieldSelector } from '../apm_common/field_selector';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
|
||||
export function ApmLatencyIndicatorTypeForm() {
|
||||
const { control, setValue, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { data: apmIndex } = useFetchApmIndex();
|
||||
useEffect(() => {
|
||||
setValue('indicator.params.index', apmIndex);
|
||||
}, [apmIndex, setValue]);
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
|
@ -119,7 +113,6 @@ export function ApmLatencyIndicatorTypeForm() {
|
|||
isInvalid={getFieldState('indicator.params.threshold').invalid}
|
||||
>
|
||||
<Controller
|
||||
shouldUnregister
|
||||
name="indicator.params.threshold"
|
||||
control={control}
|
||||
defaultValue={250}
|
||||
|
|
|
@ -9,14 +9,15 @@ import { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '@elastic
|
|||
import { EuiFlexItem, EuiIcon, EuiLoadingChart, EuiPanel } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { useDebouncedGetPreviewData } from '../../hooks/use_preview';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
|
||||
export function DataPreviewChart() {
|
||||
const { watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
const { charts, uiSettings } = useKibana().services;
|
||||
|
||||
const { data: previewData, isLoading: isPreviewLoading } = useDebouncedGetPreviewData(
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
|
||||
export interface Props {
|
||||
dataTestSubj: string;
|
||||
indexPatternString: string | undefined;
|
||||
label: string;
|
||||
name: FieldPath<CreateSLOInput>;
|
||||
name: FieldPath<CreateSLOForm>;
|
||||
placeholder: string;
|
||||
required?: boolean;
|
||||
tooltip?: ReactNode;
|
||||
|
@ -35,7 +35,7 @@ export function QueryBuilder({
|
|||
const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } =
|
||||
useKibana().services;
|
||||
|
||||
const { control, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const { dataView } = useCreateDataView({ indexPatternString });
|
||||
|
||||
|
@ -54,7 +54,6 @@ export function QueryBuilder({
|
|||
fullWidth
|
||||
>
|
||||
<Controller
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
name={name}
|
||||
control={control}
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useFetchDataViews } from '../../../../hooks/use_fetch_data_views';
|
||||
import { useFetchIndices } from '../../../../hooks/use_fetch_indices';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
|
@ -21,7 +21,7 @@ interface Option {
|
|||
}
|
||||
|
||||
export function IndexSelection() {
|
||||
const { control, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [dataViewOptions, setDataViewOptions] = useState<Option[]>([]);
|
||||
|
@ -35,8 +35,10 @@ export function IndexSelection() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDataViewOptions(createDataViewOptions(dataViews));
|
||||
}, [dataViews, dataViews.length]);
|
||||
if (dataViews.length > 0) {
|
||||
setDataViewOptions(createDataViewOptions(dataViews));
|
||||
}
|
||||
}, [dataViews]);
|
||||
|
||||
useEffect(() => {
|
||||
if (indices.length === 0) {
|
||||
|
@ -67,7 +69,7 @@ export function IndexSelection() {
|
|||
],
|
||||
});
|
||||
}
|
||||
}, [searchValue, indices, indices.length]);
|
||||
}, [indices.length, searchValue]);
|
||||
|
||||
const onDataViewSearchChange = useMemo(
|
||||
() => debounce((value: string) => setSearchValue(value), 300),
|
||||
|
@ -95,7 +97,6 @@ export function IndexSelection() {
|
|||
isInvalid={getFieldState('indicator.params.index').invalid}
|
||||
>
|
||||
<Controller
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
name="indicator.params.index"
|
||||
control={control}
|
||||
|
@ -155,20 +156,19 @@ function createDataViewLabel(dataView: DataView) {
|
|||
|
||||
function createDataViewOptions(dataViews: DataView[]): Option[] {
|
||||
const options = [];
|
||||
if (dataViews.length > 0) {
|
||||
options.push({
|
||||
label: i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.customKql.indexSelection.dataViewOptionsLabel',
|
||||
{ defaultMessage: 'Select an existing Data View' }
|
||||
),
|
||||
options: dataViews
|
||||
.map((view) => ({
|
||||
label: createDataViewLabel(view),
|
||||
value: view.getIndexPattern(),
|
||||
}))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label)),
|
||||
});
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: i18n.translate(
|
||||
'xpack.observability.slo.sloEdit.customKql.indexSelection.dataViewOptionsLabel',
|
||||
{ defaultMessage: 'Select an existing Data View' }
|
||||
),
|
||||
options: dataViews
|
||||
.map((view) => ({
|
||||
label: createDataViewLabel(view),
|
||||
value: view.getIndexPattern(),
|
||||
}))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label)),
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ import {
|
|||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
Field,
|
||||
useFetchIndexPatternFields,
|
||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { IndexSelection } from '../custom_common/index_selection';
|
||||
|
@ -31,9 +31,9 @@ interface Option {
|
|||
}
|
||||
|
||||
export function CustomKqlIndicatorTypeForm() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const index = watch('indicator.params.index');
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const index = watch('indicator.params.index');
|
||||
const { isLoading, data: indexFields } = useFetchIndexPatternFields(index);
|
||||
const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date');
|
||||
|
||||
|
@ -53,7 +53,6 @@ export function CustomKqlIndicatorTypeForm() {
|
|||
>
|
||||
<Controller
|
||||
name="indicator.params.timestampField"
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
|
@ -16,16 +15,17 @@ import {
|
|||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import {
|
||||
Field,
|
||||
useFetchIndexPatternFields,
|
||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { IndexSelection } from '../custom_common/index_selection';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { IndexSelection } from '../custom_common/index_selection';
|
||||
import { MetricIndicator } from './metric_indicator';
|
||||
|
||||
export { NEW_CUSTOM_METRIC } from './metric_indicator';
|
||||
|
||||
interface Option {
|
||||
|
@ -34,7 +34,7 @@ interface Option {
|
|||
}
|
||||
|
||||
export function CustomMetricIndicatorTypeForm() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
const { isLoading, data: indexFields } = useFetchIndexPatternFields(
|
||||
watch('indicator.params.index')
|
||||
|
@ -57,7 +57,6 @@ export function CustomMetricIndicatorTypeForm() {
|
|||
>
|
||||
<Controller
|
||||
name="indicator.params.timestampField"
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
|
@ -17,11 +16,12 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Controller, useFormContext, useFieldArray } from 'react-hook-form';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { range, first, xor } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { first, range, xor } from 'lodash';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
|
@ -68,32 +68,18 @@ export function MetricIndicator({
|
|||
metricTooltip,
|
||||
equationTooltip,
|
||||
}: MetricIndicatorProps) {
|
||||
const { control, watch, setValue } = useFormContext<CreateSLOInput>();
|
||||
|
||||
const { control, watch, setValue, register } = useFormContext<CreateSLOForm>();
|
||||
const metricFields = (indexFields ?? []).filter((field) => field.type === 'number');
|
||||
|
||||
const {
|
||||
fields: metrics,
|
||||
append,
|
||||
remove,
|
||||
} = useFieldArray({
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: `indicator.params.${type}.metrics`,
|
||||
});
|
||||
const equation = watch(`indicator.params.${type}.equation`);
|
||||
const indexPattern = watch('indicator.params.index');
|
||||
|
||||
// Without this, the hidden fields for metric.name and metric.aggregation will
|
||||
// not be included in the JSON when the form is submitted.
|
||||
useEffect(() => {
|
||||
metrics.forEach((metric, index) => {
|
||||
setValue(`indicator.params.${type}.metrics.${index}.name`, metric.name);
|
||||
setValue(`indicator.params.${type}.metrics.${index}.aggregation`, metric.aggregation);
|
||||
});
|
||||
}, [metrics, setValue, type]);
|
||||
|
||||
const disableAdd = metrics?.length === MAX_VARIABLES;
|
||||
const disableDelete = metrics?.length === 1;
|
||||
const disableAdd = fields?.length === MAX_VARIABLES;
|
||||
const disableDelete = fields?.length === 1;
|
||||
|
||||
const setDefaultEquationIfUnchanged = (previousNames: string[], nextNames: string[]) => {
|
||||
const defaultEquation = createEquationFromMetric(previousNames);
|
||||
|
@ -103,14 +89,14 @@ export function MetricIndicator({
|
|||
};
|
||||
|
||||
const handleDeleteMetric = (index: number) => () => {
|
||||
const currentVars = metrics.map((m) => m.name) ?? ['A'];
|
||||
const currentVars = fields.map((m) => m.name) ?? ['A'];
|
||||
const deletedVar = currentVars[index];
|
||||
setDefaultEquationIfUnchanged(currentVars, xor(currentVars, [deletedVar]));
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const handleAddMetric = () => {
|
||||
const currentVars = metrics.map((m) => m.name) ?? ['A'];
|
||||
const currentVars = fields.map((m) => m.name) ?? ['A'];
|
||||
const name = first(xor(VAR_NAMES, currentVars))!;
|
||||
setDefaultEquationIfUnchanged(currentVars, [...currentVars, name]);
|
||||
append({ ...NEW_CUSTOM_METRIC, name });
|
||||
|
@ -119,7 +105,7 @@ export function MetricIndicator({
|
|||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
{metrics?.map((metric, index) => (
|
||||
{fields?.map((metric, index) => (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
|
@ -131,9 +117,14 @@ export function MetricIndicator({
|
|||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
|
||||
<input
|
||||
hidden
|
||||
{...register(`indicator.params.${type}.metrics.${index}.aggregation`)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name={`indicator.params.${type}.metrics.${index}.field`}
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
|
@ -172,7 +163,6 @@ export function MetricIndicator({
|
|||
{
|
||||
value: field.value,
|
||||
label: field.value,
|
||||
'data-test-subj': `customMetricIndicatorFormMetricFieldSelectedValue`,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
@ -229,7 +219,6 @@ export function MetricIndicator({
|
|||
<EuiFlexItem>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.equation`}
|
||||
shouldUnregister
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -18,27 +15,34 @@ import {
|
|||
EuiSteps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { sloFeatureId } from '../../../../common';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { paths } from '../../../config/paths';
|
||||
import { useCreateSlo } from '../../../hooks/slo/use_create_slo';
|
||||
import { useUpdateSlo } from '../../../hooks/slo/use_update_slo';
|
||||
import { useShowSections } from '../hooks/use_show_sections';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
import { useSectionFormValidation } from '../hooks/use_section_form_validation';
|
||||
import { SloEditFormDescriptionSection } from './slo_edit_form_description_section';
|
||||
import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section';
|
||||
import { SloEditFormIndicatorSection } from './slo_edit_form_indicator_section';
|
||||
import { useUpdateSlo } from '../../../hooks/slo/use_update_slo';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
import {
|
||||
transformValuesToCreateSLOInput,
|
||||
transformSloResponseToCreateSloInput,
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
transformSloResponseToCreateSloForm,
|
||||
transformValuesToUpdateSLOInput,
|
||||
} from '../helpers/process_slo_form_values';
|
||||
import { paths } from '../../../config/paths';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
import { sloFeatureId } from '../../../../common';
|
||||
import {
|
||||
CREATE_RULE_SEARCH_PARAM,
|
||||
useAddRuleFlyoutState,
|
||||
} from '../hooks/use_add_rule_flyout_state';
|
||||
import { useCopyToJson } from '../hooks/use_copy_to_json';
|
||||
import { useParseUrlState } from '../hooks/use_parse_url_state';
|
||||
import { useSectionFormValidation } from '../hooks/use_section_form_validation';
|
||||
import { useShowSections } from '../hooks/use_show_sections';
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { SloEditFormDescriptionSection } from './slo_edit_form_description_section';
|
||||
import { SloEditFormIndicatorSection } from './slo_edit_form_indicator_section';
|
||||
import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse | undefined;
|
||||
|
@ -46,54 +50,35 @@ export interface Props {
|
|||
|
||||
export const maxWidth = 775;
|
||||
|
||||
const CREATE_RULE_SEARCH_PARAM = 'create-rule';
|
||||
|
||||
export function SloEditForm({ slo }: Props) {
|
||||
const {
|
||||
notifications,
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
|
||||
} = useKibana().services;
|
||||
|
||||
const history = useHistory();
|
||||
const { search } = useLocation();
|
||||
|
||||
const isEditMode = slo !== undefined;
|
||||
const { data: rules, isInitialLoading } = useFetchRulesForSlo({
|
||||
sloIds: slo?.id ? [slo.id] : undefined,
|
||||
});
|
||||
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
});
|
||||
|
||||
const urlParams = urlStateStorage.get<CreateSLOInput>('_a');
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
|
||||
const isEditMode = slo !== undefined;
|
||||
|
||||
const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false);
|
||||
const sloFormValuesUrlState = useParseUrlState();
|
||||
const isAddRuleFlyoutOpen = useAddRuleFlyoutState(isEditMode);
|
||||
const [isCreateRuleCheckboxChecked, setIsCreateRuleCheckboxChecked] = useState(true);
|
||||
|
||||
if (searchParams.has(CREATE_RULE_SEARCH_PARAM) && isEditMode && !isAddRuleFlyoutOpen) {
|
||||
setIsAddRuleFlyoutOpen(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && rules && rules[slo.id].length) {
|
||||
setIsCreateRuleCheckboxChecked(false);
|
||||
}
|
||||
}, [isEditMode, rules, slo]);
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: { ...SLO_EDIT_FORM_DEFAULT_VALUES, ...urlParams },
|
||||
values: transformSloResponseToCreateSloInput(slo),
|
||||
const methods = useForm<CreateSLOForm>({
|
||||
defaultValues: Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, sloFormValuesUrlState),
|
||||
values: transformSloResponseToCreateSloForm(slo),
|
||||
mode: 'all',
|
||||
});
|
||||
const { watch, getFieldState, getValues, formState, trigger } = methods;
|
||||
const handleCopyToJson = useCopyToJson({ trigger, getValues });
|
||||
|
||||
const { isIndicatorSectionValid, isObjectiveSectionValid, isDescriptionSectionValid } =
|
||||
useSectionFormValidation({
|
||||
|
@ -113,35 +98,6 @@ export function SloEditForm({ slo }: Props) {
|
|||
const { mutateAsync: createSlo, isLoading: isCreateSloLoading } = useCreateSlo();
|
||||
const { mutateAsync: updateSlo, isLoading: isUpdateSloLoading } = useUpdateSlo();
|
||||
|
||||
const handleCopyToJson = async () => {
|
||||
const isValid = await trigger();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
const values = transformValuesToCreateSLOInput(getValues());
|
||||
try {
|
||||
await copyTextToClipboard(JSON.stringify(values, null, 2));
|
||||
notifications.toasts.add({
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.copyJsonNotification', {
|
||||
defaultMessage: 'JSON copied to clipboard',
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
notifications.toasts.add({
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.copyJsonFailedNotification', {
|
||||
defaultMessage: 'Could not copy JSON to clipboard',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyTextToClipboard = async (text: string) => {
|
||||
if (!window.navigator?.clipboard) {
|
||||
throw new Error('Could not copy to clipboard!');
|
||||
}
|
||||
await window.navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const isValid = await trigger();
|
||||
if (!isValid) {
|
||||
|
@ -165,7 +121,7 @@ export function SloEditForm({ slo }: Props) {
|
|||
navigate(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
} else {
|
||||
const processedValues = transformValuesToCreateSLOInput(values);
|
||||
const processedValues = transformCreateSLOFormToCreateSLOInput(values);
|
||||
|
||||
if (isCreateRuleCheckboxChecked) {
|
||||
const { id } = await createSlo({ slo: processedValues });
|
||||
|
@ -302,7 +258,7 @@ export function SloEditForm({ slo }: Props) {
|
|||
<AddRuleFlyout
|
||||
canChangeTrigger={false}
|
||||
consumer={sloFeatureId}
|
||||
initialValues={{ name: `${slo.name} Burn Rate rule`, params: { sloId: slo.id } }}
|
||||
initialValues={{ name: `${slo.name} burn rate rule`, params: { sloId: slo.id } }}
|
||||
ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID}
|
||||
onClose={handleCloseRuleFlyout}
|
||||
onSave={handleCloseRuleFlyout}
|
||||
|
|
|
@ -19,12 +19,11 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
export function SloEditFormDescriptionSection() {
|
||||
const { control, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
const sloNameId = useGeneratedHtmlId({ prefix: 'sloName' });
|
||||
const descriptionId = useGeneratedHtmlId({ prefix: 'sloDescription' });
|
||||
const tagsId = useGeneratedHtmlId({ prefix: 'tags' });
|
||||
|
@ -107,7 +106,6 @@ export function SloEditFormDescriptionSection() {
|
|||
})}
|
||||
>
|
||||
<Controller
|
||||
shouldUnregister
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
|
|
|
@ -7,18 +7,15 @@
|
|||
|
||||
import { EuiFormRow, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { SLI_OPTIONS } from '../constants';
|
||||
import { useUnregisterFields } from '../hooks/use_unregister_fields';
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { ApmAvailabilityIndicatorTypeForm } from './apm_availability/apm_availability_indicator_type_form';
|
||||
import { ApmLatencyIndicatorTypeForm } from './apm_latency/apm_latency_indicator_type_form';
|
||||
import { CustomKqlIndicatorTypeForm } from './custom_kql/custom_kql_indicator_type_form';
|
||||
import {
|
||||
CustomMetricIndicatorTypeForm,
|
||||
NEW_CUSTOM_METRIC,
|
||||
} from './custom_metric/custom_metric_type_form';
|
||||
import { CustomMetricIndicatorTypeForm } from './custom_metric/custom_metric_type_form';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
|
||||
interface SloEditFormIndicatorSectionProps {
|
||||
|
@ -26,28 +23,8 @@ interface SloEditFormIndicatorSectionProps {
|
|||
}
|
||||
|
||||
export function SloEditFormIndicatorSection({ isEditMode }: SloEditFormIndicatorSectionProps) {
|
||||
const { control, watch, setValue } = useFormContext<CreateSLOInput>();
|
||||
|
||||
const indicator = watch('indicator.type');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditMode) {
|
||||
if (indicator === 'sli.metric.custom') {
|
||||
setValue('indicator.params.index', '');
|
||||
setValue('indicator.params.timestampField', '');
|
||||
setValue('indicator.params.good.equation', 'A');
|
||||
setValue('indicator.params.good.metrics', [NEW_CUSTOM_METRIC]);
|
||||
setValue('indicator.params.total.equation', 'A');
|
||||
setValue('indicator.params.total.metrics', [NEW_CUSTOM_METRIC]);
|
||||
}
|
||||
if (indicator === 'sli.kql.custom') {
|
||||
setValue('indicator.params.index', '');
|
||||
setValue('indicator.params.timestampField', '');
|
||||
setValue('indicator.params.good', '');
|
||||
setValue('indicator.params.total', '');
|
||||
}
|
||||
}
|
||||
}, [indicator, setValue, isEditMode]);
|
||||
const { control, watch } = useFormContext<CreateSLOForm>();
|
||||
useUnregisterFields({ isEditMode });
|
||||
|
||||
const getIndicatorTypeForm = () => {
|
||||
switch (watch('indicator.type')) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
EuiFieldNumber,
|
||||
EuiFlexGrid,
|
||||
|
@ -18,33 +17,69 @@ import {
|
|||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeWindow } from '@kbn/slo-schema';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
|
||||
import {
|
||||
BUDGETING_METHOD_OPTIONS,
|
||||
CALENDARALIGNED_TIMEWINDOW_OPTIONS,
|
||||
ROLLING_TIMEWINDOW_OPTIONS,
|
||||
TIMEWINDOW_TYPE_OPTIONS,
|
||||
} from '../constants';
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
|
||||
|
||||
export function SloEditFormObjectiveSection() {
|
||||
const { control, watch, getFieldState, resetField } = useFormContext<CreateSLOInput>();
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
getFieldState,
|
||||
setValue,
|
||||
formState: { defaultValues },
|
||||
} = useFormContext<CreateSLOForm>();
|
||||
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
|
||||
const timeWindowTypeSelect = useGeneratedHtmlId({ prefix: 'timeWindowTypeSelect' });
|
||||
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
|
||||
const timeWindowType = watch('timeWindow.type');
|
||||
|
||||
const [timeWindowTypeState, setTimeWindowTypeState] = useState<TimeWindow | undefined>(
|
||||
defaultValues?.timeWindow?.type
|
||||
);
|
||||
|
||||
/**
|
||||
* Two flow to handle: Create and Edit
|
||||
* On create: the default value is set to rolling & 30d (useForm)
|
||||
* When we change the window type (from rolling to calendar for example), we want to select a default duration (picking item 1 in the options)
|
||||
* If we don't, the select will show the option as selected, but the value is still the one from the previous window type, until the user manually changes the value
|
||||
*
|
||||
* On edit: the default value is set with the slo value
|
||||
* When we change the window type, we want to change the selected value as we do in the create flow, but we also want to fallback on the initial default value
|
||||
*
|
||||
*/
|
||||
useEffect(() => {
|
||||
resetField('timeWindow.duration', {
|
||||
defaultValue:
|
||||
timeWindowType === 'calendarAligned'
|
||||
? CALENDARALIGNED_TIMEWINDOW_OPTIONS[1].value
|
||||
: ROLLING_TIMEWINDOW_OPTIONS[1].value,
|
||||
});
|
||||
}, [timeWindowType, resetField]);
|
||||
if (timeWindowType === 'calendarAligned' && timeWindowTypeState !== timeWindowType) {
|
||||
setTimeWindowTypeState(timeWindowType);
|
||||
const exists = CALENDARALIGNED_TIMEWINDOW_OPTIONS.map((opt) => opt.value).includes(
|
||||
defaultValues?.timeWindow?.duration ?? ''
|
||||
);
|
||||
setValue(
|
||||
'timeWindow.duration',
|
||||
// @ts-ignore
|
||||
exists ? defaultValues?.timeWindow?.duration : CALENDARALIGNED_TIMEWINDOW_OPTIONS[1].value
|
||||
);
|
||||
} else if (timeWindowType === 'rolling' && timeWindowTypeState !== timeWindowType) {
|
||||
const exists = ROLLING_TIMEWINDOW_OPTIONS.map((opt) => opt.value).includes(
|
||||
defaultValues?.timeWindow?.duration ?? ''
|
||||
);
|
||||
setTimeWindowTypeState(timeWindowType);
|
||||
setValue(
|
||||
'timeWindow.duration',
|
||||
// @ts-ignore
|
||||
exists ? defaultValues?.timeWindow?.duration : ROLLING_TIMEWINDOW_OPTIONS[1].value
|
||||
);
|
||||
}
|
||||
}, [timeWindowType, setValue, defaultValues, timeWindowTypeState]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
|
@ -85,7 +120,7 @@ export function SloEditFormObjectiveSection() {
|
|||
id={timeWindowTypeSelect}
|
||||
data-test-subj="sloFormTimeWindowTypeSelect"
|
||||
options={TIMEWINDOW_TYPE_OPTIONS}
|
||||
value={String(field.value)}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -113,7 +148,6 @@ export function SloEditFormObjectiveSection() {
|
|||
<Controller
|
||||
name="timeWindow.duration"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
|
@ -126,7 +160,7 @@ export function SloEditFormObjectiveSection() {
|
|||
? CALENDARALIGNED_TIMEWINDOW_OPTIONS
|
||||
: ROLLING_TIMEWINDOW_OPTIONS
|
||||
}
|
||||
value={String(field.value)}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
export function SloEditFormObjectiveSectionTimeslices() {
|
||||
const { control, getFieldState } = useFormContext<CreateSLOInput>();
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BudgetingMethod, CreateSLOInput, TimeWindow } from '@kbn/slo-schema';
|
||||
import {
|
||||
APMTransactionDurationIndicator,
|
||||
APMTransactionErrorRateIndicator,
|
||||
BudgetingMethod,
|
||||
IndicatorType,
|
||||
KQLCustomIndicator,
|
||||
MetricCustomIndicator,
|
||||
TimeWindow,
|
||||
} from '@kbn/slo-schema';
|
||||
import {
|
||||
BUDGETING_METHOD_OCCURRENCES,
|
||||
BUDGETING_METHOD_TIMESLICES,
|
||||
|
@ -15,9 +23,10 @@ import {
|
|||
INDICATOR_CUSTOM_KQL,
|
||||
INDICATOR_CUSTOM_METRIC,
|
||||
} from '../../utils/slo/labels';
|
||||
import { CreateSLOForm } from './types';
|
||||
|
||||
export const SLI_OPTIONS: Array<{
|
||||
value: CreateSLOInput['indicator']['type'];
|
||||
value: IndicatorType;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
|
@ -87,19 +96,57 @@ export const ROLLING_TIMEWINDOW_OPTIONS = [90, 30, 7].map((number) => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOInput = {
|
||||
export const CUSTOM_KQL_DEFAULT_VALUES: KQLCustomIndicator = {
|
||||
type: 'sli.kql.custom' as const,
|
||||
params: {
|
||||
index: '',
|
||||
filter: '',
|
||||
good: '',
|
||||
total: '',
|
||||
timestampField: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const CUSTOM_METRIC_DEFAULT_VALUES: MetricCustomIndicator = {
|
||||
type: 'sli.metric.custom' as const,
|
||||
params: {
|
||||
index: '',
|
||||
filter: '',
|
||||
good: { metrics: [{ name: 'A', aggregation: 'sum' as const, field: '' }], equation: 'A' },
|
||||
total: { metrics: [{ name: 'A', aggregation: 'sum' as const, field: '' }], equation: 'A' },
|
||||
timestampField: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const APM_LATENCY_DEFAULT_VALUES: APMTransactionDurationIndicator = {
|
||||
type: 'sli.apm.transactionDuration' as const,
|
||||
params: {
|
||||
service: '',
|
||||
environment: '',
|
||||
transactionType: '',
|
||||
transactionName: '',
|
||||
threshold: 250,
|
||||
filter: '',
|
||||
index: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const APM_AVAILABILITY_DEFAULT_VALUES: APMTransactionErrorRateIndicator = {
|
||||
type: 'sli.apm.transactionErrorRate' as const,
|
||||
params: {
|
||||
service: '',
|
||||
environment: '',
|
||||
transactionType: '',
|
||||
transactionName: '',
|
||||
filter: '',
|
||||
index: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: '',
|
||||
filter: '',
|
||||
good: '',
|
||||
total: '',
|
||||
timestampField: '',
|
||||
},
|
||||
},
|
||||
indicator: CUSTOM_KQL_DEFAULT_VALUES,
|
||||
timeWindow: {
|
||||
duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
|
||||
type: 'rolling',
|
||||
|
@ -111,19 +158,10 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOInput = {
|
|||
},
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOInput = {
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
indicator: {
|
||||
type: 'sli.metric.custom',
|
||||
params: {
|
||||
index: '',
|
||||
filter: '',
|
||||
good: { metrics: [{ name: 'A', aggregation: 'sum', field: '' }], equation: 'A' },
|
||||
total: { metrics: [{ name: 'A', aggregation: 'sum', field: '' }], equation: 'A' },
|
||||
timestampField: '',
|
||||
},
|
||||
},
|
||||
indicator: CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
timeWindow: {
|
||||
duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
|
||||
type: 'rolling',
|
||||
|
|
|
@ -5,18 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import omit from 'lodash/omit';
|
||||
import type { CreateSLOInput, SLOWithSummaryResponse, UpdateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { toDuration } from '../../../utils/slo/duration';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
export function transformSloResponseToCreateSloInput(
|
||||
export function transformSloResponseToCreateSloForm(
|
||||
values: SLOWithSummaryResponse | undefined
|
||||
): CreateSLOInput | undefined {
|
||||
): CreateSLOForm | undefined {
|
||||
if (!values) return undefined;
|
||||
|
||||
return {
|
||||
...omit(values, ['id', 'revision', 'createdAt', 'updatedAt', 'summary', 'enabled']),
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
indicator: values.indicator,
|
||||
budgetingMethod: values.budgetingMethod,
|
||||
timeWindow: {
|
||||
duration: values.timeWindow.duration,
|
||||
type: values.timeWindow.type,
|
||||
},
|
||||
objective: {
|
||||
target: values.objective.target * 100,
|
||||
...(values.budgetingMethod === 'timeslices' &&
|
||||
|
@ -28,12 +34,20 @@ export function transformSloResponseToCreateSloInput(
|
|||
timesliceWindow: String(toDuration(values.objective.timesliceWindow).value),
|
||||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformValuesToCreateSLOInput(values: CreateSLOInput): CreateSLOInput {
|
||||
export function transformCreateSLOFormToCreateSLOInput(values: CreateSLOForm): CreateSLOInput {
|
||||
return {
|
||||
...values,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
indicator: values.indicator,
|
||||
budgetingMethod: values.budgetingMethod,
|
||||
timeWindow: {
|
||||
duration: values.timeWindow.duration,
|
||||
type: values.timeWindow.type,
|
||||
},
|
||||
objective: {
|
||||
target: values.objective.target / 100,
|
||||
...(values.budgetingMethod === 'timeslices' &&
|
||||
|
@ -45,12 +59,20 @@ export function transformValuesToCreateSLOInput(values: CreateSLOInput): CreateS
|
|||
timesliceWindow: `${values.objective.timesliceWindow}m`,
|
||||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformValuesToUpdateSLOInput(values: CreateSLOInput): UpdateSLOInput {
|
||||
export function transformValuesToUpdateSLOInput(values: CreateSLOForm): UpdateSLOInput {
|
||||
return {
|
||||
...values,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
indicator: values.indicator,
|
||||
budgetingMethod: values.budgetingMethod,
|
||||
timeWindow: {
|
||||
duration: values.timeWindow.duration,
|
||||
type: values.timeWindow.type,
|
||||
},
|
||||
objective: {
|
||||
target: values.objective.target / 100,
|
||||
...(values.budgetingMethod === 'timeslices' &&
|
||||
|
@ -62,5 +84,25 @@ export function transformValuesToUpdateSLOInput(values: CreateSLOInput): UpdateS
|
|||
timesliceWindow: `${values.objective.timesliceWindow}m`,
|
||||
}),
|
||||
},
|
||||
tags: values.tags,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformPartialCreateSLOInputToPartialCreateSLOForm(
|
||||
values: Partial<CreateSLOInput>
|
||||
): Partial<CreateSLOForm> {
|
||||
return {
|
||||
...values,
|
||||
...(values.objective && {
|
||||
objective: {
|
||||
target: values.objective.target * 100,
|
||||
...(values.objective.timesliceTarget && {
|
||||
timesliceTarget: values.objective.timesliceTarget * 100,
|
||||
}),
|
||||
...(values.objective.timesliceWindow && {
|
||||
timesliceWindow: String(toDuration(values.objective.timesliceWindow).value),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const CREATE_RULE_SEARCH_PARAM = 'create-rule';
|
||||
|
||||
export function useAddRuleFlyoutState(isEditMode: boolean): boolean {
|
||||
const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false);
|
||||
const { search } = useLocation();
|
||||
const searchParams = new URLSearchParams(search);
|
||||
|
||||
if (searchParams.has(CREATE_RULE_SEARCH_PARAM) && isEditMode && !isAddRuleFlyoutOpen) {
|
||||
setIsAddRuleFlyoutOpen(true);
|
||||
}
|
||||
|
||||
return isAddRuleFlyoutOpen;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { UseFormGetValues, UseFormTrigger } from 'react-hook-form';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { transformCreateSLOFormToCreateSLOInput } from '../helpers/process_slo_form_values';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
interface Props {
|
||||
trigger: UseFormTrigger<CreateSLOForm>;
|
||||
getValues: UseFormGetValues<CreateSLOForm>;
|
||||
}
|
||||
|
||||
export function useCopyToJson({ trigger, getValues }: Props): () => Promise<void> {
|
||||
const { notifications } = useKibana().services;
|
||||
|
||||
const handleCopyToJson = async () => {
|
||||
const isValid = await trigger();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
const values = transformCreateSLOFormToCreateSLOInput(getValues());
|
||||
try {
|
||||
await copyTextToClipboard(JSON.stringify(values, null, 2));
|
||||
notifications.toasts.add({
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.copyJsonNotification', {
|
||||
defaultMessage: 'JSON copied to clipboard',
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
notifications.toasts.add({
|
||||
title: i18n.translate('xpack.observability.slo.sloEdit.copyJsonFailedNotification', {
|
||||
defaultMessage: 'Could not copy JSON to clipboard',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyTextToClipboard = async (text: string) => {
|
||||
if (!window.navigator?.clipboard) {
|
||||
throw new Error('Could not copy to clipboard!');
|
||||
}
|
||||
await window.navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
return handleCopyToJson;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { transformPartialCreateSLOInputToPartialCreateSLOForm } from '../helpers/process_slo_form_values';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
export function useParseUrlState(): Partial<CreateSLOForm> | null {
|
||||
const history = useHistory();
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
});
|
||||
|
||||
const urlParams = urlStateStorage.get<Partial<CreateSLOInput>>('_a');
|
||||
|
||||
return !!urlParams ? transformPartialCreateSLOInputToPartialCreateSLOForm(urlParams) : null;
|
||||
}
|
|
@ -5,15 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateSLOInput, MetricCustomIndicator } from '@kbn/slo-schema';
|
||||
import { MetricCustomIndicator } from '@kbn/slo-schema';
|
||||
import { FormState, UseFormGetFieldState, UseFormGetValues, UseFormWatch } from 'react-hook-form';
|
||||
import { isObject } from 'lodash';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
interface Props {
|
||||
getFieldState: UseFormGetFieldState<CreateSLOInput>;
|
||||
getValues: UseFormGetValues<CreateSLOInput>;
|
||||
formState: FormState<CreateSLOInput>;
|
||||
watch: UseFormWatch<CreateSLOInput>;
|
||||
getFieldState: UseFormGetFieldState<CreateSLOForm>;
|
||||
getValues: UseFormGetValues<CreateSLOForm>;
|
||||
formState: FormState<CreateSLOForm>;
|
||||
watch: UseFormWatch<CreateSLOForm>;
|
||||
}
|
||||
|
||||
export function useSectionFormValidation({ getFieldState, getValues, formState, watch }: Props) {
|
||||
|
@ -59,7 +60,6 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
|
|||
[
|
||||
'indicator.params.index',
|
||||
'indicator.params.filter',
|
||||
|
||||
'indicator.params.total',
|
||||
'indicator.params.timestampField',
|
||||
] as const
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { IndicatorType } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import deepmerge from 'deepmerge';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFetchApmIndex } from '../../../hooks/slo/use_fetch_apm_indices';
|
||||
import {
|
||||
APM_AVAILABILITY_DEFAULT_VALUES,
|
||||
APM_LATENCY_DEFAULT_VALUES,
|
||||
CUSTOM_KQL_DEFAULT_VALUES,
|
||||
CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
} from '../constants';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
/**
|
||||
* This hook handles the unregistration of inputs when selecting another SLI indicator.
|
||||
* We could not use shouldUnregister on the controlled form fields because of a bug when submitting the form
|
||||
* which was unmounting the components and therefore unregistering the associated values.
|
||||
*/
|
||||
export function useUnregisterFields({ isEditMode }: { isEditMode: boolean }) {
|
||||
const { data: apmIndex } = useFetchApmIndex();
|
||||
const { watch, unregister, reset, resetField } = useFormContext<CreateSLOForm>();
|
||||
const [indicatorTypeState, setIndicatorTypeState] = useState<IndicatorType>(
|
||||
watch('indicator.type')
|
||||
);
|
||||
const indicatorType = watch('indicator.type');
|
||||
|
||||
useEffect(() => {
|
||||
if (indicatorType !== indicatorTypeState && !isEditMode) {
|
||||
setIndicatorTypeState(indicatorType);
|
||||
unregister('indicator.params');
|
||||
switch (indicatorType) {
|
||||
case 'sli.metric.custom':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
indicator: CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
}),
|
||||
{
|
||||
keepDefaultValues: true,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'sli.kql.custom':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
indicator: CUSTOM_KQL_DEFAULT_VALUES,
|
||||
}),
|
||||
{
|
||||
keepDefaultValues: true,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'sli.apm.transactionDuration':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
indicator: deepmerge(APM_LATENCY_DEFAULT_VALUES, { params: { index: apmIndex } }),
|
||||
}),
|
||||
{
|
||||
keepDefaultValues: true,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
reset(
|
||||
Object.assign({}, SLO_EDIT_FORM_DEFAULT_VALUES, {
|
||||
indicator: deepmerge(APM_AVAILABILITY_DEFAULT_VALUES, {
|
||||
params: { index: apmIndex },
|
||||
}),
|
||||
}),
|
||||
{
|
||||
keepDefaultValues: true,
|
||||
}
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(indicatorType);
|
||||
}
|
||||
}
|
||||
}, [isEditMode, indicatorType, indicatorTypeState, unregister, reset, resetField, apmIndex]);
|
||||
}
|
25
x-pack/plugins/observability/public/pages/slo_edit/types.ts
Normal file
25
x-pack/plugins/observability/public/pages/slo_edit/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { BudgetingMethod, Indicator, TimeWindow } from '@kbn/slo-schema';
|
||||
|
||||
export interface CreateSLOForm {
|
||||
name: string;
|
||||
description: string;
|
||||
indicator: Indicator;
|
||||
timeWindow: {
|
||||
duration: string;
|
||||
type: TimeWindow;
|
||||
};
|
||||
tags: string[];
|
||||
budgetingMethod: BudgetingMethod;
|
||||
objective: {
|
||||
target: number;
|
||||
timesliceTarget?: number;
|
||||
timesliceWindow?: string;
|
||||
};
|
||||
}
|
|
@ -30,8 +30,8 @@ import { SloSummary } from './slo_summary';
|
|||
import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal';
|
||||
import { SloBadges } from './badges/slo_badges';
|
||||
import {
|
||||
transformSloResponseToCreateSloInput,
|
||||
transformValuesToCreateSLOInput,
|
||||
transformSloResponseToCreateSloForm,
|
||||
transformCreateSLOFormToCreateSLOInput,
|
||||
} from '../../slo_edit/helpers/process_slo_form_values';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { rulesLocatorID, sloFeatureId } from '../../../../common';
|
||||
|
@ -111,8 +111,8 @@ export function SloListItem({
|
|||
};
|
||||
|
||||
const handleClone = () => {
|
||||
const newSlo = transformValuesToCreateSLOInput(
|
||||
transformSloResponseToCreateSloInput({ ...slo, name: `[Copy] ${slo.name}` })!
|
||||
const newSlo = transformCreateSLOFormToCreateSLOInput(
|
||||
transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })!
|
||||
);
|
||||
|
||||
cloneSlo({ slo: newSlo, originalSloId: slo.id });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue