mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
feat(slo): allow configuration of advanced settings from UI (#200822)
This commit is contained in:
parent
78842b7c1c
commit
8fe4c44192
65 changed files with 1333 additions and 853 deletions
|
@ -48663,19 +48663,23 @@ components:
|
|||
properties:
|
||||
frequency:
|
||||
default: 1m
|
||||
description: Configure how often the transform runs, default 1m
|
||||
description: The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.
|
||||
example: 5m
|
||||
type: string
|
||||
preventInitialBackfill:
|
||||
default: false
|
||||
description: Prevents the transform from backfilling data when it starts.
|
||||
description: Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.
|
||||
example: true
|
||||
type: boolean
|
||||
syncDelay:
|
||||
default: 1m
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
description: The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.
|
||||
example: 5m
|
||||
type: string
|
||||
syncField:
|
||||
description: The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.
|
||||
example: event.ingested
|
||||
type: string
|
||||
title: Settings
|
||||
type: object
|
||||
SLOs_slo_definition_response:
|
||||
|
|
|
@ -56371,19 +56371,23 @@ components:
|
|||
properties:
|
||||
frequency:
|
||||
default: 1m
|
||||
description: Configure how often the transform runs, default 1m
|
||||
description: The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.
|
||||
example: 5m
|
||||
type: string
|
||||
preventInitialBackfill:
|
||||
default: false
|
||||
description: Prevents the transform from backfilling data when it starts.
|
||||
description: Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.
|
||||
example: true
|
||||
type: boolean
|
||||
syncDelay:
|
||||
default: 1m
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
description: The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.
|
||||
example: 5m
|
||||
type: string
|
||||
syncField:
|
||||
description: The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.
|
||||
example: event.ingested
|
||||
type: string
|
||||
title: Settings
|
||||
type: object
|
||||
SLOs_slo_definition_response:
|
||||
|
|
|
@ -27,16 +27,26 @@ const objectiveSchema = t.intersection([
|
|||
t.partial({ timesliceTarget: t.number, timesliceWindow: durationType }),
|
||||
]);
|
||||
|
||||
const settingsSchema = t.type({
|
||||
syncDelay: durationType,
|
||||
frequency: durationType,
|
||||
preventInitialBackfill: t.boolean,
|
||||
});
|
||||
const settingsSchema = t.intersection([
|
||||
t.type({
|
||||
syncDelay: durationType,
|
||||
frequency: durationType,
|
||||
preventInitialBackfill: t.boolean,
|
||||
}),
|
||||
t.partial({ syncField: t.union([t.string, t.null]) }),
|
||||
]);
|
||||
|
||||
const groupBySchema = allOrAnyStringOrArray;
|
||||
|
||||
const optionalSettingsSchema = t.partial({ ...settingsSchema.props });
|
||||
const optionalSettingsSchema = t.partial({
|
||||
syncDelay: durationType,
|
||||
frequency: durationType,
|
||||
preventInitialBackfill: t.boolean,
|
||||
syncField: t.union([t.string, t.null]),
|
||||
});
|
||||
|
||||
const tagsSchema = t.array(t.string);
|
||||
|
||||
// id cannot contain special characters and spaces
|
||||
const sloIdSchema = new t.Type<string, string, unknown>(
|
||||
'sloIdSchema',
|
||||
|
|
|
@ -1647,20 +1647,25 @@
|
|||
"description": "Defines properties for SLO settings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"syncField": {
|
||||
"description": "The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.",
|
||||
"type": "string",
|
||||
"example": "event.ingested"
|
||||
},
|
||||
"syncDelay": {
|
||||
"description": "The synch delay to apply to the transform. Default 1m",
|
||||
"description": "The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.",
|
||||
"type": "string",
|
||||
"default": "1m",
|
||||
"example": "5m"
|
||||
},
|
||||
"frequency": {
|
||||
"description": "Configure how often the transform runs, default 1m",
|
||||
"description": "The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.",
|
||||
"type": "string",
|
||||
"default": "1m",
|
||||
"example": "5m"
|
||||
},
|
||||
"preventInitialBackfill": {
|
||||
"description": "Prevents the transform from backfilling data when it starts.",
|
||||
"description": "Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"example": true
|
||||
|
|
|
@ -1137,18 +1137,22 @@ components:
|
|||
description: Defines properties for SLO settings.
|
||||
type: object
|
||||
properties:
|
||||
syncField:
|
||||
description: The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.
|
||||
type: string
|
||||
example: event.ingested
|
||||
syncDelay:
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
description: The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.
|
||||
type: string
|
||||
default: 1m
|
||||
example: 5m
|
||||
frequency:
|
||||
description: Configure how often the transform runs, default 1m
|
||||
description: The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.
|
||||
type: string
|
||||
default: 1m
|
||||
example: 5m
|
||||
preventInitialBackfill:
|
||||
description: Prevents the transform from backfilling data when it starts.
|
||||
description: Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.
|
||||
type: boolean
|
||||
default: false
|
||||
example: true
|
||||
|
|
|
@ -2,18 +2,22 @@ title: Settings
|
|||
description: Defines properties for SLO settings.
|
||||
type: object
|
||||
properties:
|
||||
syncField:
|
||||
description: The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.
|
||||
type: string
|
||||
example: 'event.ingested'
|
||||
syncDelay:
|
||||
description: The synch delay to apply to the transform. Default 1m
|
||||
description: The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.
|
||||
type: string
|
||||
default: 1m
|
||||
example: 5m
|
||||
frequency:
|
||||
description: Configure how often the transform runs, default 1m
|
||||
description: The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.
|
||||
type: string
|
||||
default: 1m
|
||||
example: 5m
|
||||
preventInitialBackfill:
|
||||
description: Prevents the transform from backfilling data when it starts.
|
||||
description: Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.
|
||||
type: boolean
|
||||
default: false
|
||||
example: true
|
||||
|
|
|
@ -33,17 +33,3 @@ paths:
|
|||
# $ref: "paths/s@{spaceid}@api@slos@_definitions.yaml"
|
||||
"/s/{spaceId}/api/observability/slos/_delete_instances":
|
||||
$ref: "paths/s@{spaceid}@api@slos@_delete_instances.yaml"
|
||||
# Security is defined when files are joined in oas_docs
|
||||
# components:
|
||||
# securitySchemes:
|
||||
# basicAuth:
|
||||
# type: http
|
||||
# scheme: basic
|
||||
# apiKeyAuth:
|
||||
# type: apiKey
|
||||
# in: header
|
||||
# name: Authorization
|
||||
# description: 'e.g. Authorization: ApiKey base64AccessApiKey'
|
||||
# security:
|
||||
# - basicAuth: []
|
||||
# - apiKeyAuth: []
|
||||
|
|
|
@ -39,6 +39,7 @@ const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
|
|||
good: 'http_status: 2xx',
|
||||
total: 'a query',
|
||||
timestampField: 'custom_timestamp',
|
||||
dataViewId: 'some-data-view-id',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
|
|
|
@ -11,16 +11,21 @@ import { SloEditLocatorDefinition } from './slo_edit';
|
|||
describe('SloEditLocator', () => {
|
||||
const locator = new SloEditLocatorDefinition();
|
||||
|
||||
it('should return correct url when empty params are provided', async () => {
|
||||
it('returns the correct url when empty params are provided', async () => {
|
||||
const location = await locator.getLocation({});
|
||||
expect(location.app).toEqual('slo');
|
||||
expect(location.path).toEqual('/create?_a=()');
|
||||
});
|
||||
|
||||
it('should return correct url when slo is provided', async () => {
|
||||
const location = await locator.getLocation(buildSlo({ id: 'foo' }));
|
||||
it('returns the correct url when slo id is provided', async () => {
|
||||
const location = await locator.getLocation({ id: 'existing-slo-id' });
|
||||
expect(location.path).toEqual('/edit/existing-slo-id');
|
||||
});
|
||||
|
||||
it('returns the correct url when partial slo input is provided', async () => {
|
||||
const location = await locator.getLocation(buildSlo({ id: undefined }));
|
||||
expect(location.path).toEqual(
|
||||
"/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',groupings:(),id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',meta:(),name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',preventInitialBackfill:!f,syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)"
|
||||
"/create?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',groupings:(),indicator:(params:(dataViewId:some-data-view-id,filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',meta:(),name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',preventInitialBackfill:!f,syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,31 +5,34 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { RecursivePartial } from '@elastic/charts';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition } from '@kbn/share-plugin/public';
|
||||
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
import { sloEditLocatorID } from '@kbn/observability-plugin/common';
|
||||
import type { CreateSLOForm } from '../pages/slo_edit/types';
|
||||
import type { LocatorDefinition } from '@kbn/share-plugin/public';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { SLO_CREATE_PATH } from '../../common/locators/paths';
|
||||
|
||||
export type SloEditParams = RecursivePartial<CreateSLOForm>;
|
||||
|
||||
export interface SloEditLocatorParams extends SloEditParams, SerializableRecord {}
|
||||
export type SloEditLocatorParams = RecursivePartial<CreateSLOInput>;
|
||||
|
||||
export class SloEditLocatorDefinition implements LocatorDefinition<SloEditLocatorParams> {
|
||||
public readonly id = sloEditLocatorID;
|
||||
|
||||
public readonly getLocation = async (slo: SloEditLocatorParams) => {
|
||||
if (!!slo.id) {
|
||||
return {
|
||||
app: 'slo',
|
||||
path: `/edit/${encodeURIComponent(slo.id)}`,
|
||||
state: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
app: 'slo',
|
||||
path: setStateToKbnUrl<SloEditParams>(
|
||||
path: setStateToKbnUrl<RecursivePartial<CreateSLOInput>>(
|
||||
'_a',
|
||||
{
|
||||
...slo,
|
||||
},
|
||||
slo,
|
||||
{ useHash: false, storeInHashQuery: false },
|
||||
slo.id ? `/edit/${encodeURIComponent(String(slo.id))}` : `${SLO_CREATE_PATH}`
|
||||
SLO_CREATE_PATH
|
||||
),
|
||||
state: {},
|
||||
};
|
||||
|
|
|
@ -8,15 +8,14 @@
|
|||
import { EuiFlexGrid, EuiPanel, EuiText, useIsWithinBreakpoints } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TagsList } from '@kbn/observability-shared-plugin/public';
|
||||
import {
|
||||
SLOWithSummaryResponse,
|
||||
occurrencesBudgetingMethodSchema,
|
||||
querySchema,
|
||||
rollingTimeWindowTypeSchema,
|
||||
SLOWithSummaryResponse,
|
||||
} from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { TagsList } from '@kbn/observability-shared-plugin/public';
|
||||
import { DisplayQuery } from './display_query';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import {
|
||||
BUDGETING_METHOD_OCCURRENCES,
|
||||
|
@ -26,9 +25,9 @@ import {
|
|||
toIndicatorTypeLabel,
|
||||
} from '../../../../utils/slo/labels';
|
||||
import { ApmIndicatorOverview } from './apm_indicator_overview';
|
||||
import { SyntheticsIndicatorOverview } from './synthetics_indicator_overview';
|
||||
|
||||
import { DisplayQuery } from './display_query';
|
||||
import { OverviewItem } from './overview_item';
|
||||
import { SyntheticsIndicatorOverview } from './synthetics_indicator_overview';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
|
@ -170,6 +169,19 @@ export function Overview({ slo }: Props) {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OverviewItem
|
||||
title={i18n.translate('xpack.slo.sloDetails.overview.settings.syncDelay', {
|
||||
defaultMessage: 'Sync delay',
|
||||
})}
|
||||
subtitle={slo.settings.syncDelay}
|
||||
/>
|
||||
<OverviewItem
|
||||
title={i18n.translate('xpack.slo.sloDetails.overview.settings.frequency', {
|
||||
defaultMessage: 'Frequency',
|
||||
})}
|
||||
subtitle={slo.settings.frequency}
|
||||
/>
|
||||
</EuiFlexGrid>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiAccordion,
|
||||
EuiCheckbox,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { SyncFieldSelector } from './sync_field_selector';
|
||||
|
||||
export function AdvancedSettings() {
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
const preventBackfillCheckbox = useGeneratedHtmlId({ prefix: 'preventBackfill' });
|
||||
const advancedSettingsAccordion = useGeneratedHtmlId({ prefix: 'advancedSettingsAccordion' });
|
||||
|
||||
return (
|
||||
<EuiAccordion
|
||||
paddingSize="s"
|
||||
id={advancedSettingsAccordion}
|
||||
buttonContent={
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="controlsVertical" size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.advancedSettingsLabel', {
|
||||
defaultMessage: 'Advanced settings',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGrid columns={3} gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<SyncFieldSelector />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
isInvalid={getFieldState('settings.syncDelay').invalid}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.syncDelay.label', {
|
||||
defaultMessage: 'Sync delay (in minutes)',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.settings.syncDelay.tooltip', {
|
||||
defaultMessage:
|
||||
'The time delay in minutes between the current time and the latest source data time. Increasing the value will delay any alerting. The default value is 1 minute. The minimum value is 1m and the maximum is 359m. It should always be greater then source index refresh interval.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="settings.syncDelay"
|
||||
defaultValue={1}
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 359 }}
|
||||
render={({ field: { ref, onChange, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
data-test-subj="sloAdvancedSettingsSyncDelay"
|
||||
isInvalid={fieldState.invalid}
|
||||
required
|
||||
value={field.value}
|
||||
min={1}
|
||||
max={359}
|
||||
step={1}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
isInvalid={getFieldState('settings.frequency').invalid}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.frequency.label', {
|
||||
defaultMessage: 'Frequency (in minutes)',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.settings.frequency.tooltip', {
|
||||
defaultMessage:
|
||||
'The interval between checks for changes in the source data. The minimum value is 1m and the maximum is 59m. The default value is 1 minute.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="settings.frequency"
|
||||
defaultValue={1}
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 59 }}
|
||||
render={({ field: { ref, onChange, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
data-test-subj="sloAdvancedSettingsFrequency"
|
||||
isInvalid={fieldState.invalid}
|
||||
required
|
||||
value={field.value}
|
||||
min={1}
|
||||
max={59}
|
||||
step={1}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiFormRow isInvalid={getFieldState('settings.preventInitialBackfill').invalid}>
|
||||
<Controller
|
||||
name="settings.preventInitialBackfill"
|
||||
control={control}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<EuiCheckbox
|
||||
id={preventBackfillCheckbox}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.preventInitialBackfill.label', {
|
||||
defaultMessage: 'Prevent initial backfill of data',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.slo.sloEdit.settings.preventInitialBackfill.tooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
checked={Boolean(field.value)}
|
||||
onChange={(event: any) => onChange(event.target.checked)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexGroup>
|
||||
</EuiAccordion>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiIconTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { createOptionsFromFields } from '../../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { OptionalText } from '../../common/optional_text';
|
||||
|
||||
const placeholder = i18n.translate('xpack.slo.sloEdit.settings.syncField.placeholder', {
|
||||
defaultMessage: 'Select a timestamp field',
|
||||
});
|
||||
|
||||
export function SyncFieldSelector() {
|
||||
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
const [index, dataViewId] = watch(['indicator.params.index', 'indicator.params.dataViewId']);
|
||||
const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({
|
||||
indexPatternString: index,
|
||||
dataViewId,
|
||||
});
|
||||
const timestampFields = dataView?.fields?.filter((field) => field.type === 'date') ?? [];
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.syncField.label', {
|
||||
defaultMessage: 'Sync field',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.settings.syncField.tooltip', {
|
||||
defaultMessage:
|
||||
'The date field that is used to identify new documents in the source. It is strongly recommended to use a field that contains the ingest timestamp. If you use a different field, you might need to set the delay such that it accounts for data transmission delays. When unspecified, we use the indicator timestamp field.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
isInvalid={getFieldState('settings.syncField').invalid}
|
||||
labelAppend={<OptionalText />}
|
||||
>
|
||||
<Controller
|
||||
name={'settings.syncField'}
|
||||
defaultValue={null}
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<EuiComboBox<string>
|
||||
{...field}
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
isClearable
|
||||
isDisabled={isIndexFieldsLoading}
|
||||
isInvalid={fieldState.invalid}
|
||||
isLoading={isIndexFieldsLoading}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
|
||||
field.onChange(null);
|
||||
}}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={createOptionsFromFields(timestampFields)}
|
||||
selectedOptions={
|
||||
!!timestampFields && !!field.value
|
||||
? [{ value: field.value, label: field.value }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { ApmAvailabilityIndicatorTypeForm as Component } from './apm_availability_indicator_type_form';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -12,14 +12,14 @@ import React from 'react';
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { useApmDefaultValues } from '../apm_common/use_apm_default_values';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { useFetchApmIndex } from '../../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { FieldSelector } from '../apm_common/field_selector';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { formatAllFilters } from '../../helpers/format_filters';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { formatAllFilters } from '../../../helpers/format_filters';
|
||||
import { getGroupByCardinalityFilters } from '../apm_common/get_group_by_cardinality_filters';
|
||||
|
||||
export function ApmAvailabilityIndicatorTypeForm() {
|
||||
|
@ -56,8 +56,8 @@ export function ApmAvailabilityIndicatorTypeForm() {
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<FieldSelector
|
||||
allowAllOption={false}
|
||||
label={i18n.translate('xpack.slo.sloEdit.apmAvailability.serviceName', {
|
||||
|
@ -94,7 +94,7 @@ export function ApmAvailabilityIndicatorTypeForm() {
|
|||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<FieldSelector
|
||||
label={i18n.translate('xpack.slo.sloEdit.apmAvailability.transactionType', {
|
||||
defaultMessage: 'Transaction type',
|
||||
|
@ -125,7 +125,7 @@ export function ApmAvailabilityIndicatorTypeForm() {
|
|||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<QueryBuilder
|
||||
dataTestSubj="apmLatencyFilterInput"
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { FieldSelector as Component, Props } from './field_selector';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -11,8 +11,8 @@ import { ALL_VALUE } from '@kbn/slo-schema';
|
|||
import { debounce } from 'lodash';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import { Suggestion, useFetchApmSuggestions } from '../../../../hooks/use_fetch_apm_suggestions';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { Suggestion, useFetchApmSuggestions } from '../../../../../hooks/use_fetch_apm_suggestions';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
|
@ -8,8 +8,8 @@
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { ALL_VALUE, APMTransactionErrorRateIndicator } from '@kbn/slo-schema';
|
||||
import { useEffect } from 'react';
|
||||
import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { useFetchApmIndex } from '../../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
|
||||
export const useApmDefaultValues = () => {
|
||||
const { watch, setValue } = useFormContext<CreateSLOForm<APMTransactionErrorRateIndicator>>();
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { ApmLatencyIndicatorTypeForm as Component } from './apm_latency_indicator_type_form';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -12,14 +12,14 @@ import React from 'react';
|
|||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useApmDefaultValues } from '../apm_common/use_apm_default_values';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { useFetchApmIndex } from '../../../../../hooks/use_fetch_apm_indices';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { FieldSelector } from '../apm_common/field_selector';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { formatAllFilters } from '../../helpers/format_filters';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { formatAllFilters } from '../../../helpers/format_filters';
|
||||
import { getGroupByCardinalityFilters } from '../apm_common/get_group_by_cardinality_filters';
|
||||
|
||||
export function ApmLatencyIndicatorTypeForm() {
|
||||
|
@ -58,8 +58,8 @@ export function ApmLatencyIndicatorTypeForm() {
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<FieldSelector
|
||||
allowAllOption={false}
|
||||
label={i18n.translate('xpack.slo.sloEdit.apmLatency.serviceName', {
|
||||
|
@ -96,7 +96,7 @@ export function ApmLatencyIndicatorTypeForm() {
|
|||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<FieldSelector
|
||||
label={i18n.translate('xpack.slo.sloEdit.apmLatency.transactionType', {
|
||||
defaultMessage: 'Transaction type',
|
|
@ -9,17 +9,16 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { TimestampFieldSelector } from '../common/timestamp_field_selector';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { TimestampFieldSelector } from '../../common/timestamp_field_selector';
|
||||
import { IndexSelection } from './index_selection';
|
||||
|
||||
export function IndexAndTimestampField({
|
||||
dataView,
|
||||
isLoading,
|
||||
}: {
|
||||
interface Props {
|
||||
dataView?: DataView;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
}
|
||||
|
||||
export function IndexAndTimestampField({ dataView, isLoading }: Props) {
|
||||
const { watch } = useFormContext<CreateSLOForm>();
|
||||
const index = watch('indicator.params.index');
|
||||
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { IndexSelection as Component } from './index_selection';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -8,37 +8,47 @@
|
|||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
|
||||
import { getDataViewPattern, useAdhocDataViews } from './use_adhoc_data_views';
|
||||
import { SLOPublicPluginsStart } from '../../../..';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { SLOPublicPluginsStart } from '../../../../..';
|
||||
import { useKibana } from '../../../../../hooks/use_kibana';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { getDataViewPatternOrId, useAdhocDataViews } from './use_adhoc_data_views';
|
||||
|
||||
const BTN_MAX_WIDTH = 515;
|
||||
|
||||
export const DATA_VIEW_FIELD = 'indicator.params.dataViewId';
|
||||
const INDEX_FIELD = 'indicator.params.index';
|
||||
const TIMESTAMP_FIELD = 'indicator.params.timestampField';
|
||||
const INDICATOR_TIMESTAMP_FIELD = 'indicator.params.timestampField';
|
||||
const GROUP_BY_FIELD = 'groupBy';
|
||||
const SETTINGS_SYNC_FIELD = 'settings.syncField';
|
||||
|
||||
export function IndexSelection({ selectedDataView }: { selectedDataView?: DataView }) {
|
||||
const { control, getFieldState, setValue, watch } = useFormContext<CreateSLOForm>();
|
||||
const { dataViews: dataViewsService, dataViewFieldEditor } = useKibana().services;
|
||||
|
||||
const { dataViewEditor } = useKibana<SLOPublicPluginsStart>().services;
|
||||
const {
|
||||
dataViews: dataViewsService,
|
||||
dataViewFieldEditor,
|
||||
dataViewEditor,
|
||||
} = useKibana<SLOPublicPluginsStart>().services;
|
||||
|
||||
const currentIndexPattern = watch(INDEX_FIELD);
|
||||
const currentDataViewId = watch(DATA_VIEW_FIELD);
|
||||
|
||||
const { dataViewsList, isDataViewsLoading, adHocDataViews, setAdHocDataViews, refetch } =
|
||||
useAdhocDataViews({
|
||||
currentIndexPattern,
|
||||
});
|
||||
const {
|
||||
dataViewsList,
|
||||
isDataViewsLoading,
|
||||
adHocDataViews,
|
||||
setAdHocDataViews,
|
||||
refetchDataViewsList,
|
||||
} = useAdhocDataViews({
|
||||
currentIndexPattern,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const indPatternId = getDataViewPattern({
|
||||
byPatten: currentIndexPattern,
|
||||
const indPatternId = getDataViewPatternOrId({
|
||||
byPattern: currentIndexPattern,
|
||||
dataViewsList,
|
||||
adHocDataViews,
|
||||
});
|
||||
|
@ -54,13 +64,24 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi
|
|||
setValue,
|
||||
]);
|
||||
|
||||
const updateDataViewDependantFields = (indexPattern?: string, timestampField?: string) => {
|
||||
setValue(INDEX_FIELD, indexPattern ?? '');
|
||||
setValue(INDICATOR_TIMESTAMP_FIELD, timestampField ?? '');
|
||||
setValue(GROUP_BY_FIELD, ALL_VALUE);
|
||||
setValue(SETTINGS_SYNC_FIELD, null);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow label={INDEX_LABEL} isInvalid={getFieldState(INDEX_FIELD).invalid} fullWidth>
|
||||
<EuiFormRow
|
||||
label={INDEX_LABEL}
|
||||
isInvalid={getFieldState(INDEX_FIELD).invalid || getFieldState(DATA_VIEW_FIELD).invalid}
|
||||
fullWidth
|
||||
>
|
||||
<Controller
|
||||
defaultValue=""
|
||||
name={DATA_VIEW_FIELD}
|
||||
control={control}
|
||||
rules={{ required: !Boolean(currentIndexPattern) }}
|
||||
rules={{ required: true }}
|
||||
render={({ field, fieldState }) => (
|
||||
<DataViewPicker
|
||||
adHocDataViews={adHocDataViews}
|
||||
|
@ -72,15 +93,13 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi
|
|||
style: { width: '100%', maxWidth: BTN_MAX_WIDTH },
|
||||
}}
|
||||
onChangeDataView={(newId: string) => {
|
||||
setValue(
|
||||
INDEX_FIELD,
|
||||
getDataViewPattern({ byId: newId, adHocDataViews, dataViewsList })!
|
||||
);
|
||||
field.onChange(newId);
|
||||
|
||||
dataViewsService.get(newId).then((dataView) => {
|
||||
if (dataView.timeFieldName) {
|
||||
setValue(TIMESTAMP_FIELD, dataView.timeFieldName);
|
||||
}
|
||||
updateDataViewDependantFields(
|
||||
getDataViewPatternOrId({ byId: newId, adHocDataViews, dataViewsList })!,
|
||||
dataView.timeFieldName
|
||||
);
|
||||
});
|
||||
}}
|
||||
onAddField={
|
||||
|
@ -97,8 +116,8 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi
|
|||
}
|
||||
currentDataViewId={
|
||||
field.value ??
|
||||
getDataViewPattern({
|
||||
byPatten: currentIndexPattern,
|
||||
getDataViewPatternOrId({
|
||||
byPattern: currentIndexPattern,
|
||||
dataViewsList,
|
||||
adHocDataViews,
|
||||
})
|
||||
|
@ -108,17 +127,13 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi
|
|||
allowAdHocDataView: true,
|
||||
onSave: (dataView: DataView) => {
|
||||
if (!dataView.isPersisted()) {
|
||||
setAdHocDataViews([...adHocDataViews, dataView]);
|
||||
field.onChange(dataView.id);
|
||||
setValue(INDEX_FIELD, dataView.getIndexPattern());
|
||||
setAdHocDataViews((prev) => [...prev, dataView]);
|
||||
} else {
|
||||
refetch();
|
||||
field.onChange(dataView.id);
|
||||
setValue(INDEX_FIELD, dataView.getIndexPattern());
|
||||
}
|
||||
if (dataView.timeFieldName) {
|
||||
setValue(TIMESTAMP_FIELD, dataView.timeFieldName);
|
||||
refetchDataViewsList();
|
||||
}
|
||||
|
||||
field.onChange(dataView.id);
|
||||
updateDataViewDependantFields(dataView.getIndexPattern(), dataView.timeFieldName);
|
||||
},
|
||||
});
|
||||
}}
|
|
@ -8,16 +8,16 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { DataView, DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { useFetchDataViews } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { useKibana } from '../../../../../hooks/use_kibana';
|
||||
|
||||
export const getDataViewPattern = ({
|
||||
export const getDataViewPatternOrId = ({
|
||||
byId,
|
||||
byPatten,
|
||||
byPattern,
|
||||
dataViewsList,
|
||||
adHocDataViews,
|
||||
}: {
|
||||
byId?: string;
|
||||
byPatten?: string;
|
||||
byPattern?: string;
|
||||
dataViewsList: DataViewListItem[];
|
||||
adHocDataViews: DataView[];
|
||||
}) => {
|
||||
|
@ -28,20 +28,24 @@ export const getDataViewPattern = ({
|
|||
if (byId) {
|
||||
return allDataViews.find((dv) => dv.id === byId)?.title;
|
||||
}
|
||||
if (byPatten) {
|
||||
return allDataViews.find((dv) => dv.title === byPatten)?.id;
|
||||
if (byPattern) {
|
||||
return allDataViews.find((dv) => dv.title === byPattern)?.id;
|
||||
}
|
||||
};
|
||||
|
||||
export const useAdhocDataViews = ({ currentIndexPattern }: { currentIndexPattern: string }) => {
|
||||
const { isLoading: isDataViewsLoading, data: dataViewsList = [], refetch } = useFetchDataViews();
|
||||
const {
|
||||
isLoading: isDataViewsLoading,
|
||||
data: dataViewsList = [],
|
||||
refetch: refetchDataViewsList,
|
||||
} = useFetchDataViews();
|
||||
const { dataViews: dataViewsService } = useKibana().services;
|
||||
const [adHocDataViews, setAdHocDataViews] = useState<DataView[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDataViewsLoading) {
|
||||
const missingDataView = getDataViewPattern({
|
||||
byPatten: currentIndexPattern,
|
||||
const missingDataView = getDataViewPatternOrId({
|
||||
byPattern: currentIndexPattern,
|
||||
dataViewsList,
|
||||
adHocDataViews,
|
||||
});
|
||||
|
@ -70,6 +74,6 @@ export const useAdhocDataViews = ({ currentIndexPattern }: { currentIndexPattern
|
|||
setAdHocDataViews,
|
||||
dataViewsList,
|
||||
isDataViewsLoading,
|
||||
refetch,
|
||||
refetchDataViewsList,
|
||||
};
|
||||
};
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { CustomKqlIndicatorTypeForm as Component } from './custom_kql_indicator_type_form';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -9,12 +9,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
|
||||
export function CustomKqlIndicatorTypeForm() {
|
||||
|
@ -28,7 +28,7 @@ export function CustomKqlIndicatorTypeForm() {
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<IndexAndTimestampField dataView={dataView} isLoading={isIndexFieldsLoading} />
|
||||
|
||||
<EuiFlexItem>
|
|
@ -9,9 +9,9 @@ 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 { KibanaReactStorybookDecorator } from '../../../../../utils/kibana_react.storybook_decorator';
|
||||
import { CustomMetricIndicatorTypeForm as Component } from './custom_metric_type_form';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC } from '../../constants';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC } from '../../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
|
@ -18,11 +18,11 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { MetricIndicator } from './metric_indicator';
|
||||
|
||||
|
@ -55,7 +55,7 @@ export function CustomMetricIndicatorTypeForm() {
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<IndexAndTimestampField dataView={dataView} isLoading={isIndexFieldsLoading} />
|
||||
|
||||
<EuiFlexItem>
|
|
@ -26,10 +26,10 @@ import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
|||
import {
|
||||
aggValueToLabel,
|
||||
CUSTOM_METRIC_AGGREGATION_OPTIONS,
|
||||
} from '../../helpers/aggregation_options';
|
||||
import { createOptionsFromFields, Option } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
} from '../../../helpers/aggregation_options';
|
||||
import { createOptionsFromFields, Option } from '../../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
|
||||
interface MetricIndicatorProps {
|
||||
type: 'good' | 'total';
|
||||
|
@ -134,95 +134,28 @@ export function MetricIndicator({
|
|||
<EuiFlexItem>
|
||||
{fields?.map((metric, index, arr) => (
|
||||
<div key={metric.id}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" key={metric.id}>
|
||||
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={
|
||||
getFieldState(`indicator.params.${type}.metrics.${index}.aggregation`).invalid
|
||||
}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', {
|
||||
defaultMessage: 'Aggregation',
|
||||
})}{' '}
|
||||
{metric.name}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.metrics.${index}.aggregation`}
|
||||
defaultValue="sum"
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
async
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
isClearable
|
||||
isInvalid={fieldState.invalid}
|
||||
isDisabled={isLoadingIndex || !indexPattern}
|
||||
isLoading={!!indexPattern && isLoadingIndex}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
field.onChange('');
|
||||
}}
|
||||
selectedOptions={
|
||||
!!indexPattern &&
|
||||
!!field.value &&
|
||||
CUSTOM_METRIC_AGGREGATION_OPTIONS.some((agg) => agg.value === field.value)
|
||||
? [
|
||||
{
|
||||
value: field.value,
|
||||
label: aggValueToLabel(field.value),
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
setAggregationOptions(
|
||||
CUSTOM_METRIC_AGGREGATION_OPTIONS.filter(({ value }) =>
|
||||
value.includes(searchValue)
|
||||
)
|
||||
);
|
||||
}}
|
||||
options={aggregationOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" key={metric.id}>
|
||||
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={
|
||||
getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid
|
||||
getFieldState(`indicator.params.${type}.metrics.${index}.aggregation`).invalid
|
||||
}
|
||||
label={
|
||||
<span>
|
||||
{metricLabel} {metric.name} {metricTooltip}
|
||||
{i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', {
|
||||
defaultMessage: 'Aggregation',
|
||||
})}{' '}
|
||||
{metric.name}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.metrics.${index}.field`}
|
||||
defaultValue=""
|
||||
name={`indicator.params.${type}.metrics.${index}.aggregation`}
|
||||
defaultValue="sum"
|
||||
rules={{ required: true }}
|
||||
shouldUnregister
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
|
@ -231,12 +164,12 @@ export function MetricIndicator({
|
|||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a metric field' }
|
||||
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a metric field' }
|
||||
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
|
||||
{ defaultMessage: 'Select an aggregation' }
|
||||
)}
|
||||
isClearable
|
||||
isInvalid={fieldState.invalid}
|
||||
|
@ -251,64 +184,138 @@ export function MetricIndicator({
|
|||
selectedOptions={
|
||||
!!indexPattern &&
|
||||
!!field.value &&
|
||||
metricFields.some((metricField) => metricField.name === field.value)
|
||||
CUSTOM_METRIC_AGGREGATION_OPTIONS.some(
|
||||
(agg) => agg.value === field.value
|
||||
)
|
||||
? [
|
||||
{
|
||||
value: field.value,
|
||||
label: field.value,
|
||||
label: aggValueToLabel(field.value),
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
setOptions(
|
||||
createOptionsFromFields(metricFields, ({ value }) =>
|
||||
setAggregationOptions(
|
||||
CUSTOM_METRIC_AGGREGATION_OPTIONS.filter(({ value }) =>
|
||||
value.includes(searchValue)
|
||||
)
|
||||
);
|
||||
}}
|
||||
options={options}
|
||||
options={aggregationOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="o11yMetricIndicatorButton"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
style={{ marginTop: '1.5em' }}
|
||||
onClick={handleDeleteMetric(index)}
|
||||
disabled={disableDelete}
|
||||
title={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
|
||||
defaultMessage: 'Delete metric',
|
||||
})}
|
||||
aria-label={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
|
||||
defaultMessage: 'Delete metric',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && (
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={
|
||||
getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid
|
||||
}
|
||||
label={
|
||||
<span>
|
||||
{metricLabel} {metric.name} {metricTooltip}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={`indicator.params.${type}.metrics.${index}.field`}
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
shouldUnregister
|
||||
control={control}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
async
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a metric field' }
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
|
||||
{ defaultMessage: 'Select a metric field' }
|
||||
)}
|
||||
isClearable
|
||||
isInvalid={fieldState.invalid}
|
||||
isDisabled={isLoadingIndex || !indexPattern}
|
||||
isLoading={!!indexPattern && isLoadingIndex}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
field.onChange('');
|
||||
}}
|
||||
selectedOptions={
|
||||
!!indexPattern &&
|
||||
!!field.value &&
|
||||
metricFields.some((metricField) => metricField.name === field.value)
|
||||
? [
|
||||
{
|
||||
value: field.value,
|
||||
label: field.value,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
onSearchChange={(searchValue: string) => {
|
||||
setOptions(
|
||||
createOptionsFromFields(metricFields, ({ value }) =>
|
||||
value.includes(searchValue)
|
||||
)
|
||||
);
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="o11yMetricIndicatorButton"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
style={{ marginTop: '1.5em' }}
|
||||
onClick={handleDeleteMetric(index)}
|
||||
disabled={disableDelete}
|
||||
title={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
|
||||
defaultMessage: 'Delete metric',
|
||||
})}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.customMetric.deleteLabel',
|
||||
{
|
||||
defaultMessage: 'Delete metric',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<QueryBuilder
|
||||
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
|
||||
dataView={dataView}
|
||||
label={`${filterLabel} ${metric.name}`}
|
||||
name={`indicator.params.${type}.metrics.${index}.filter`}
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.placeholder', {
|
||||
defaultMessage: 'KQL filter',
|
||||
})}
|
||||
required={false}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.tooltip', {
|
||||
defaultMessage: 'This KQL query should return a subset of events.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
<QueryBuilder
|
||||
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
|
||||
dataView={dataView}
|
||||
label={`${filterLabel} ${metric.name}`}
|
||||
name={`indicator.params.${type}.metrics.${index}.filter`}
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.placeholder', {
|
||||
defaultMessage: 'KQL filter',
|
||||
})}
|
||||
required={false}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.tooltip', {
|
||||
defaultMessage: 'This KQL query should return a subset of events.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{index !== arr.length - 1 && <EuiHorizontalRule size="quarter" />}
|
||||
</div>
|
||||
))}
|
|
@ -19,9 +19,9 @@ import { DataView, FieldSpec } from '@kbn/data-views-plugin/common';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { createOptionsFromFields, Option } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { createOptionsFromFields, Option } from '../../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
|
||||
interface HistogramIndicatorProps {
|
||||
type: 'good' | 'total';
|
|
@ -18,11 +18,11 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { HistogramIndicator } from './histogram_indicator';
|
||||
|
||||
|
@ -49,7 +49,7 @@ export function HistogramIndicatorTypeForm() {
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<IndexAndTimestampField dataView={dataView} isLoading={isIndexFieldsLoading} />
|
||||
|
||||
<EuiFlexItem>
|
|
@ -17,12 +17,12 @@ import moment from 'moment';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { formatAllFilters } from '../../helpers/format_filters';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { GroupByCardinality } from '../common/group_by_cardinality';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
import { formatAllFilters } from '../../../helpers/format_filters';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { GroupByCardinality } from '../../common/group_by_cardinality';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { FieldSelector } from '../synthetics_common/field_selector';
|
||||
|
||||
export function SyntheticsAvailabilityIndicatorTypeForm() {
|
||||
|
@ -74,8 +74,8 @@ export function SyntheticsAvailabilityIndicatorTypeForm() {
|
|||
}, [currentMonitors, setValue]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<FieldSelector
|
||||
required
|
||||
allowAllOption
|
||||
|
@ -130,7 +130,7 @@ export function SyntheticsAvailabilityIndicatorTypeForm() {
|
|||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<QueryBuilder
|
||||
dataTestSubj="syntheticsAvailabilityFilterInput"
|
|
@ -11,12 +11,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ALL_VALUE, SyntheticsAvailabilityIndicator } from '@kbn/slo-schema';
|
||||
import { debounce } from 'lodash';
|
||||
import { Controller, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import { OptionalText } from '../common/optional_text';
|
||||
import { OptionalText } from '../../common/optional_text';
|
||||
import {
|
||||
useFetchSyntheticsSuggestions,
|
||||
Suggestion,
|
||||
} from '../../../../hooks/use_fetch_synthetics_suggestions';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
} from '../../../../../hooks/use_fetch_synthetics_suggestions';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
|
@ -24,9 +24,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { first, range, xor } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { COMPARATOR_OPTIONS } from '../../constants';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { COMPARATOR_OPTIONS } from '../../../constants';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { MetricInput } from './metric_input';
|
||||
|
||||
interface MetricIndicatorProps {
|
||||
|
@ -117,54 +117,56 @@ export function MetricIndicator({ indexFields, isLoadingIndex, dataView }: Metri
|
|||
<EuiFlexItem>
|
||||
{fields?.map((metric, index, arr) => (
|
||||
<React.Fragment key={metric.id}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<input hidden {...register(`indicator.params.metric.metrics.${index}.name`)} />
|
||||
<MetricInput
|
||||
isLoadingIndex={isLoadingIndex}
|
||||
metricIndex={index}
|
||||
indexPattern={indexPattern}
|
||||
indexFields={indexFields}
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<input hidden {...register(`indicator.params.metric.metrics.${index}.name`)} />
|
||||
<MetricInput
|
||||
isLoadingIndex={isLoadingIndex}
|
||||
metricIndex={index}
|
||||
indexPattern={indexPattern}
|
||||
indexFields={indexFields}
|
||||
/>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="o11yMetricIndicatorButton"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
style={{ marginTop: '1.5em' }}
|
||||
onClick={handleDeleteMetric(index)}
|
||||
disabled={disableDelete}
|
||||
title={i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.deleteLabel', {
|
||||
defaultMessage: 'Delete metric',
|
||||
})}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.deleteLabel',
|
||||
{ defaultMessage: 'Delete metric' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<QueryBuilder
|
||||
dataTestSubj="timesliceMetricIndicatorFormMetricQueryInput"
|
||||
dataView={dataView}
|
||||
label={`${filterLabel} ${metric.name}`}
|
||||
name={`indicator.params.metric.metrics.${index}.filter`}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.placeholder',
|
||||
{ defaultMessage: 'KQL filter' }
|
||||
)}
|
||||
required={false}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.tooltip',
|
||||
{
|
||||
defaultMessage: 'This KQL query should return a subset of events.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="o11yMetricIndicatorButton"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
style={{ marginTop: '1.5em' }}
|
||||
onClick={handleDeleteMetric(index)}
|
||||
disabled={disableDelete}
|
||||
title={i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.deleteLabel', {
|
||||
defaultMessage: 'Delete metric',
|
||||
})}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.deleteLabel',
|
||||
{ defaultMessage: 'Delete metric' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<QueryBuilder
|
||||
dataTestSubj="timesliceMetricIndicatorFormMetricQueryInput"
|
||||
dataView={dataView}
|
||||
label={`${filterLabel} ${metric.name}`}
|
||||
name={`indicator.params.metric.metrics.${index}.filter`}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.placeholder',
|
||||
{ defaultMessage: 'KQL filter' }
|
||||
)}
|
||||
required={false}
|
||||
tooltip={
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.tooltip',
|
||||
{
|
||||
defaultMessage: 'This KQL query should return a subset of events.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{index !== arr.length - 1 && <EuiHorizontalRule size="quarter" />}
|
||||
</React.Fragment>
|
||||
))}
|
|
@ -16,9 +16,9 @@ import { FieldSpec } from '@kbn/data-views-plugin/common';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { AGGREGATION_OPTIONS, aggValueToLabel } from '../../helpers/aggregation_options';
|
||||
import { createOptionsFromFields, Option } from '../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { AGGREGATION_OPTIONS, aggValueToLabel } from '../../../helpers/aggregation_options';
|
||||
import { createOptionsFromFields, Option } from '../../../helpers/create_options';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
|
||||
const fieldLabel = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
|
@ -19,15 +19,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { GroupByField } from '../common/group_by_field';
|
||||
import { CreateSLOForm } from '../../types';
|
||||
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../common/query_builder';
|
||||
import { useKibana } from '../../../../../hooks/use_kibana';
|
||||
import { GroupByField } from '../../common/group_by_field';
|
||||
import { CreateSLOForm } from '../../../types';
|
||||
import { DataPreviewChart } from '../../common/data_preview_chart';
|
||||
import { QueryBuilder } from '../../common/query_builder';
|
||||
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
|
||||
import { MetricIndicator } from './metric_indicator';
|
||||
import { COMPARATOR_MAPPING } from '../../constants';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { COMPARATOR_MAPPING } from '../../../constants';
|
||||
import { useCreateDataView } from '../../../../../hooks/use_create_data_view';
|
||||
|
||||
export { NEW_TIMESLICE_METRIC } from './metric_indicator';
|
||||
|
||||
|
@ -54,7 +54,7 @@ export function TimesliceMetricIndicatorTypeForm() {
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<IndexAndTimestampField dataView={dataView} isLoading={isIndexFieldsLoading} />
|
||||
|
||||
<EuiFlexItem>
|
|
@ -5,43 +5,56 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiSpacer, EuiSteps } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiSteps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import type { CreateSLOInput, GetSLOResponse } from '@kbn/slo-schema';
|
||||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import React from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import { SloEditFormFooter } from './slo_edit_form_footer';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
|
||||
import { transformSloResponseToCreateSloForm } from '../helpers/process_slo_form_values';
|
||||
import {
|
||||
transformPartialSLOStateToFormState,
|
||||
transformSloResponseToCreateSloForm,
|
||||
} from '../helpers/process_slo_form_values';
|
||||
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 { SloEditFormFooter } from './slo_edit_form_footer';
|
||||
import { SloEditFormIndicatorSection } from './slo_edit_form_indicator_section';
|
||||
import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section';
|
||||
|
||||
export interface Props {
|
||||
slo?: GetSLOResponse;
|
||||
initialValues?: RecursivePartial<CreateSLOForm>;
|
||||
initialValues?: RecursivePartial<CreateSLOInput>;
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
export const maxWidth = 900;
|
||||
|
||||
export function SloEditForm({ slo, initialValues, onSave }: Props) {
|
||||
const isEditMode = slo !== undefined;
|
||||
const isFlyoutMode = initialValues !== undefined && onSave !== undefined;
|
||||
|
||||
const sloFormValuesFromUrlState = useParseUrlState() ?? (initialValues as CreateSLOForm);
|
||||
const sloFormValuesFromFlyoutState = isFlyoutMode
|
||||
? transformPartialSLOStateToFormState(initialValues)
|
||||
: undefined;
|
||||
const sloFormValuesFromUrlState = useParseUrlState();
|
||||
const sloFormValuesFromSloResponse = transformSloResponseToCreateSloForm(slo);
|
||||
|
||||
const methods = useForm<CreateSLOForm>({
|
||||
defaultValues: sloFormValuesFromUrlState ?? SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
values: sloFormValuesFromUrlState ? sloFormValuesFromUrlState : sloFormValuesFromSloResponse,
|
||||
const form = useForm<CreateSLOForm>({
|
||||
defaultValues: isFlyoutMode
|
||||
? sloFormValuesFromFlyoutState
|
||||
: sloFormValuesFromUrlState
|
||||
? sloFormValuesFromUrlState
|
||||
: sloFormValuesFromSloResponse ?? SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
values: isFlyoutMode
|
||||
? sloFormValuesFromFlyoutState
|
||||
: sloFormValuesFromUrlState
|
||||
? sloFormValuesFromUrlState
|
||||
: sloFormValuesFromSloResponse,
|
||||
mode: 'all',
|
||||
});
|
||||
const { watch, getFieldState, getValues, formState } = methods;
|
||||
const { watch, getFieldState, getValues, formState } = form;
|
||||
|
||||
const { isIndicatorSectionValid, isObjectiveSectionValid, isDescriptionSectionValid } =
|
||||
useSectionFormValidation({
|
||||
|
@ -59,41 +72,37 @@ export function SloEditForm({ slo, initialValues, onSave }: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj="sloForm">
|
||||
<EuiSteps
|
||||
steps={[
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.definition.title', {
|
||||
defaultMessage: 'Define SLI',
|
||||
}),
|
||||
children: <SloEditFormIndicatorSection isEditMode={isEditMode} />,
|
||||
status: isIndicatorSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.objectives.title', {
|
||||
defaultMessage: 'Set objectives',
|
||||
}),
|
||||
children: showObjectiveSection ? <SloEditFormObjectiveSection /> : null,
|
||||
status: showObjectiveSection && isObjectiveSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.description.title', {
|
||||
defaultMessage: 'Describe SLO',
|
||||
}),
|
||||
children: showDescriptionSection ? <SloEditFormDescriptionSection /> : null,
|
||||
status:
|
||||
showDescriptionSection && isDescriptionSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<FormProvider {...form}>
|
||||
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="sloForm">
|
||||
<EuiSteps
|
||||
steps={[
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.definition.title', {
|
||||
defaultMessage: 'Define SLI',
|
||||
}),
|
||||
children: <SloEditFormIndicatorSection isEditMode={isEditMode} />,
|
||||
status: isIndicatorSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.objectives.title', {
|
||||
defaultMessage: 'Set objectives',
|
||||
}),
|
||||
children: showObjectiveSection ? <SloEditFormObjectiveSection /> : null,
|
||||
status: showObjectiveSection && isObjectiveSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.slo.sloEdit.description.title', {
|
||||
defaultMessage: 'Describe SLO',
|
||||
}),
|
||||
children: showDescriptionSection ? <SloEditFormDescriptionSection /> : null,
|
||||
status:
|
||||
showDescriptionSection && isDescriptionSectionValid ? 'complete' : 'incomplete',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<SloEditFormFooter slo={slo} onSave={onSave} />
|
||||
</EuiFlexGroup>
|
||||
</FormProvider>
|
||||
</>
|
||||
<SloEditFormFooter slo={slo} onSave={onSave} />
|
||||
</EuiFlexGroup>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import {
|
|||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiTextArea,
|
||||
|
@ -20,9 +18,9 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useFetchSLOSuggestions } from '../hooks/use_fetch_suggestions';
|
||||
import { OptionalText } from './common/optional_text';
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
import { OptionalText } from './common/optional_text';
|
||||
import { MAX_WIDTH } from '../constants';
|
||||
|
||||
export function SloEditFormDescriptionSection() {
|
||||
const { control, getFieldState } = useFormContext<CreateSLOForm>();
|
||||
|
@ -37,129 +35,117 @@ export function SloEditFormDescriptionSection() {
|
|||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
style={{ maxWidth }}
|
||||
style={{ maxWidth: MAX_WIDTH }}
|
||||
data-test-subj="sloEditFormDescriptionSection"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={getFieldState('name').invalid}
|
||||
label={i18n.translate('xpack.slo.sloEdit.description.sloName', {
|
||||
defaultMessage: 'SLO Name',
|
||||
})}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldText
|
||||
{...field}
|
||||
fullWidth
|
||||
isInvalid={fieldState.invalid}
|
||||
id={sloNameId}
|
||||
data-test-subj="sloFormNameInput"
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.description.sloNamePlaceholder', {
|
||||
defaultMessage: 'Name for the SLO',
|
||||
})}
|
||||
/>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={getFieldState('name').invalid}
|
||||
label={i18n.translate('xpack.slo.sloEdit.description.sloName', {
|
||||
defaultMessage: 'SLO Name',
|
||||
})}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiFieldText
|
||||
{...field}
|
||||
fullWidth
|
||||
isInvalid={fieldState.invalid}
|
||||
id={sloNameId}
|
||||
data-test-subj="sloFormNameInput"
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.description.sloNamePlaceholder', {
|
||||
defaultMessage: 'Name for the SLO',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloEdit.description.sloDescription', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
labelAppend={<OptionalText />}
|
||||
>
|
||||
<Controller
|
||||
name="description"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiTextArea
|
||||
{...field}
|
||||
fullWidth
|
||||
id={descriptionId}
|
||||
data-test-subj="sloFormDescriptionTextArea"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.description.sloDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'A short description of the SLO',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloEdit.description.sloDescription', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
labelAppend={<OptionalText />}
|
||||
>
|
||||
<Controller
|
||||
name="description"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiTextArea
|
||||
{...field}
|
||||
fullWidth
|
||||
id={descriptionId}
|
||||
data-test-subj="sloFormDescriptionTextArea"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.slo.sloEdit.description.sloDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'A short description of the SLO',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloEdit.tags.label', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
id={tagsId}
|
||||
fullWidth
|
||||
aria-label={i18n.translate('xpack.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
isInvalid={fieldState.invalid}
|
||||
options={suggestions?.tags ?? []}
|
||||
selectedOptions={generateTagOptions(field.value)}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected.map((opts) => opts.value));
|
||||
}
|
||||
|
||||
field.onChange([]);
|
||||
}}
|
||||
onCreateOption={(searchValue: string, options: EuiComboBoxOptionOption[] = []) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const values = field.value ?? [];
|
||||
|
||||
if (
|
||||
values.findIndex((tag) => tag.trim().toLowerCase() === normalizedSearchValue) ===
|
||||
-1
|
||||
) {
|
||||
field.onChange([...values, searchValue]);
|
||||
}
|
||||
}}
|
||||
isClearable
|
||||
data-test-subj="sloEditTagsSelector"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloEdit.tags.label', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{ required: false }}
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
id={tagsId}
|
||||
fullWidth
|
||||
aria-label={i18n.translate('xpack.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.slo.sloEdit.tags.placeholder', {
|
||||
defaultMessage: 'Add tags',
|
||||
})}
|
||||
isInvalid={fieldState.invalid}
|
||||
options={suggestions?.tags ?? []}
|
||||
selectedOptions={generateTagOptions(field.value)}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected.map((opts) => opts.value));
|
||||
}
|
||||
|
||||
field.onChange([]);
|
||||
}}
|
||||
onCreateOption={(
|
||||
searchValue: string,
|
||||
options: EuiComboBoxOptionOption[] = []
|
||||
) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const values = field.value ?? [];
|
||||
|
||||
if (
|
||||
values.findIndex(
|
||||
(tag) => tag.trim().toLowerCase() === normalizedSearchValue
|
||||
) === -1
|
||||
) {
|
||||
field.onChange([...values, searchValue]);
|
||||
}
|
||||
}}
|
||||
isClearable
|
||||
data-test-subj="sloEditTagsSelector"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,19 +7,20 @@
|
|||
|
||||
import { EuiFormRow, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import React, { useMemo } 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 { SyntheticsAvailabilityIndicatorTypeForm } from './synthetics_availability/synthetics_availability_indicator_type_form';
|
||||
import { CustomKqlIndicatorTypeForm } from './custom_kql/custom_kql_indicator_type_form';
|
||||
import { CustomMetricIndicatorTypeForm } from './custom_metric/custom_metric_type_form';
|
||||
import { HistogramIndicatorTypeForm } from './histogram/histogram_indicator_type_form';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
import { TimesliceMetricIndicatorTypeForm } from './timeslice_metric/timeslice_metric_indicator';
|
||||
import { MAX_WIDTH } from '../constants';
|
||||
import { ApmAvailabilityIndicatorTypeForm } from './indicator_section/apm_availability/apm_availability_indicator_type_form';
|
||||
import { ApmLatencyIndicatorTypeForm } from './indicator_section/apm_latency/apm_latency_indicator_type_form';
|
||||
import { CustomKqlIndicatorTypeForm } from './indicator_section/custom_kql/custom_kql_indicator_type_form';
|
||||
import { CustomMetricIndicatorTypeForm } from './indicator_section/custom_metric/custom_metric_type_form';
|
||||
import { HistogramIndicatorTypeForm } from './indicator_section/histogram/histogram_indicator_type_form';
|
||||
import { SyntheticsAvailabilityIndicatorTypeForm } from './indicator_section/synthetics_availability/synthetics_availability_indicator_type_form';
|
||||
import { TimesliceMetricIndicatorTypeForm } from './indicator_section/timeslice_metric/timeslice_metric_indicator';
|
||||
|
||||
interface SloEditFormIndicatorSectionProps {
|
||||
isEditMode: boolean;
|
||||
|
@ -48,7 +49,7 @@ export function SloEditFormIndicatorSection({ isEditMode }: SloEditFormIndicator
|
|||
case 'sli.metric.timeslice':
|
||||
return <TimesliceMetricIndicatorTypeForm />;
|
||||
default:
|
||||
return null;
|
||||
assertNever(indicatorType);
|
||||
}
|
||||
}, [indicatorType]);
|
||||
|
||||
|
@ -57,7 +58,7 @@ export function SloEditFormIndicatorSection({ isEditMode }: SloEditFormIndicator
|
|||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
style={{ maxWidth }}
|
||||
style={{ maxWidth: MAX_WIDTH }}
|
||||
data-test-subj="sloEditFormIndicatorSection"
|
||||
>
|
||||
{!isEditMode && (
|
||||
|
@ -78,7 +79,7 @@ export function SloEditFormIndicatorSection({ isEditMode }: SloEditFormIndicator
|
|||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xl" />
|
||||
</>
|
||||
)}
|
||||
{indicatorTypeForm}
|
||||
|
|
|
@ -7,15 +7,14 @@
|
|||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiCheckbox,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -30,7 +29,8 @@ import {
|
|||
TIMEWINDOW_TYPE_OPTIONS,
|
||||
} from '../constants';
|
||||
import { CreateSLOForm } from '../types';
|
||||
import { maxWidth } from './slo_edit_form';
|
||||
import { MAX_WIDTH } from '../constants';
|
||||
import { AdvancedSettings } from './indicator_section/advanced_settings/advanced_settings';
|
||||
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
|
||||
|
||||
export function SloEditFormObjectiveSection() {
|
||||
|
@ -44,7 +44,6 @@ export function SloEditFormObjectiveSection() {
|
|||
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
|
||||
const timeWindowTypeSelect = useGeneratedHtmlId({ prefix: 'timeWindowTypeSelect' });
|
||||
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
|
||||
const preventBackfillCheckbox = useGeneratedHtmlId({ prefix: 'preventBackfill' });
|
||||
const timeWindowType = watch('timeWindow.type');
|
||||
const indicator = watch('indicator.type');
|
||||
|
||||
|
@ -91,237 +90,199 @@ export function SloEditFormObjectiveSection() {
|
|||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
style={{ maxWidth }}
|
||||
style={{ maxWidth: MAX_WIDTH }}
|
||||
data-test-subj="sloEditFormObjectiveSection"
|
||||
>
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.timeWindowType.label', {
|
||||
defaultMessage: 'Time window',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.timeWindowType.tooltip', {
|
||||
defaultMessage: 'Choose between a rolling or a calendar aligned window.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="timeWindow.type"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={timeWindowTypeSelect}
|
||||
data-test-subj="sloFormTimeWindowTypeSelect"
|
||||
options={TIMEWINDOW_TYPE_OPTIONS}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.timeWindowDuration.label', {
|
||||
defaultMessage: 'Duration',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.timeWindowDuration.tooltip', {
|
||||
defaultMessage: 'The time window duration used to compute the SLO over.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="timeWindow.duration"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={timeWindowSelect}
|
||||
data-test-subj="sloFormTimeWindowDurationSelect"
|
||||
options={
|
||||
timeWindowType === 'calendarAligned'
|
||||
? CALENDARALIGNED_TIMEWINDOW_OPTIONS
|
||||
: ROLLING_TIMEWINDOW_OPTIONS
|
||||
}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
{indicator === 'sli.metric.timeslice' && (
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEdit.sliType.timesliceMetric.objectiveMessage"
|
||||
defaultMessage="The timeslice metric requires the budgeting method to be set to 'Timeslices' due to the nature of the statistical aggregations. The 'timeslice target' is also ignored in favor of the 'threshold' set in the metric definition above. The 'timeslice window' will set the size of the window the aggregation is performed on."
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGrid columns={3} gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.timeWindowType.label', {
|
||||
defaultMessage: 'Time window',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.timeWindowType.tooltip', {
|
||||
defaultMessage: 'Choose between a rolling or a calendar aligned window.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="timeWindow.type"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={timeWindowTypeSelect}
|
||||
data-test-subj="sloFormTimeWindowTypeSelect"
|
||||
options={TIMEWINDOW_TYPE_OPTIONS}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{indicator === 'sli.synthetics.availability' && (
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEdit.sliType.syntheticAvailability.objectiveMessage"
|
||||
defaultMessage="The Synthetics availability indicator requires the budgeting method to be set to 'Occurrences'."
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.timeWindowDuration.label', {
|
||||
defaultMessage: 'Duration',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.timeWindowDuration.tooltip', {
|
||||
defaultMessage: 'The time window duration used to compute the SLO over.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="timeWindow.duration"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
required
|
||||
id={timeWindowSelect}
|
||||
data-test-subj="sloFormTimeWindowDurationSelect"
|
||||
options={
|
||||
timeWindowType === 'calendarAligned'
|
||||
? CALENDARALIGNED_TIMEWINDOW_OPTIONS
|
||||
: ROLLING_TIMEWINDOW_OPTIONS
|
||||
}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.budgetingMethod.label', {
|
||||
defaultMessage: 'Budgeting method',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.budgetingMethod.tooltip', {
|
||||
defaultMessage:
|
||||
'Occurrences-based SLO uses the ratio of good events over the total events during the time window. Timeslices-based SLO uses the ratio of good time slices over the total time slices during the time window.',
|
||||
})}
|
||||
position="top"
|
||||
{indicator === 'sli.metric.timeslice' && (
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEdit.sliType.timesliceMetric.objectiveMessage"
|
||||
defaultMessage="The timeslice metric requires the budgeting method to be set to 'Timeslices' due to the nature of the statistical aggregations. The 'timeslice target' is also ignored in favor of the 'threshold' set in the metric definition above. The 'timeslice window' will set the size of the window the aggregation is performed on."
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="budgetingMethod"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
disabled={
|
||||
indicator === 'sli.metric.timeslice' ||
|
||||
indicator === 'sli.synthetics.availability'
|
||||
}
|
||||
required
|
||||
id={budgetingSelect}
|
||||
data-test-subj="sloFormBudgetingMethodSelect"
|
||||
options={BUDGETING_METHOD_OPTIONS}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{indicator === 'sli.synthetics.availability' && (
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEdit.sliType.syntheticAvailability.objectiveMessage"
|
||||
defaultMessage="The Synthetics availability indicator requires the budgeting method to be set to 'Occurrences'."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{watch('budgetingMethod') === 'timeslices' ? (
|
||||
<SloEditFormObjectiveSectionTimeslices />
|
||||
) : null}
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexGrid columns={3} gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.budgetingMethod.label', {
|
||||
defaultMessage: 'Budgeting method',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.budgetingMethod.tooltip', {
|
||||
defaultMessage:
|
||||
'Occurrences-based SLO uses the ratio of good events over the total events during the time window. Timeslices-based SLO uses the ratio of good time slices over the total time slices during the time window.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="budgetingMethod"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<EuiSelect
|
||||
{...field}
|
||||
disabled={
|
||||
indicator === 'sli.metric.timeslice' ||
|
||||
indicator === 'sli.synthetics.availability'
|
||||
}
|
||||
required
|
||||
id={budgetingSelect}
|
||||
data-test-subj="sloFormBudgetingMethodSelect"
|
||||
options={BUDGETING_METHOD_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
{watch('budgetingMethod') === 'timeslices' ? (
|
||||
<SloEditFormObjectiveSectionTimeslices />
|
||||
) : null}
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
isInvalid={getFieldState('objective.target').invalid}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.targetSlo.label', {
|
||||
defaultMessage: 'Target / SLO (%)',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.targetSlo.tooltip', {
|
||||
defaultMessage: 'The target objective in percentage for the SLO.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="objective.target"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
min: 0.001,
|
||||
max: 99.999,
|
||||
}}
|
||||
render={({ field: { ref, onChange, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
required
|
||||
isInvalid={fieldState.invalid}
|
||||
data-test-subj="sloFormObjectiveTargetInput"
|
||||
value={field.value}
|
||||
min={0.001}
|
||||
max={99.999}
|
||||
step={0.001}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexGrid columns={3} gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
isInvalid={getFieldState('objective.target').invalid}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.targetSlo.label', {
|
||||
defaultMessage: 'Target / SLO (%)',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.slo.sloEdit.targetSlo.tooltip', {
|
||||
defaultMessage: 'The target objective in percentage for the SLO.',
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="objective.target"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
min: 0.001,
|
||||
max: 99.999,
|
||||
}}
|
||||
render={({ field: { ref, onChange, ...field }, fieldState }) => (
|
||||
<EuiFieldNumber
|
||||
{...field}
|
||||
required
|
||||
isInvalid={fieldState.invalid}
|
||||
data-test-subj="sloFormObjectiveTargetInput"
|
||||
value={field.value}
|
||||
min={0.001}
|
||||
max={99.999}
|
||||
step={0.001}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGrid columns={3}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow isInvalid={getFieldState('settings.preventInitialBackfill').invalid}>
|
||||
<Controller
|
||||
name="settings.preventInitialBackfill"
|
||||
control={control}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<EuiCheckbox
|
||||
id={preventBackfillCheckbox}
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.slo.sloEdit.settings.preventInitialBackfill.label', {
|
||||
defaultMessage: 'Prevent initial backfill of data',
|
||||
})}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.slo.sloEdit.settings.preventInitialBackfill.tooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Start aggregating data from the time the SLO is created, instead of backfilling data from the beginning of the time window.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
checked={Boolean(field.value)}
|
||||
onChange={(event: any) => onChange(event.target.checked)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<AdvancedSettings />
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ import {
|
|||
import { SYNTHETICS_DEFAULT_GROUPINGS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
|
||||
import { CreateSLOForm } from './types';
|
||||
|
||||
export const MAX_WIDTH = 900;
|
||||
|
||||
export const SLI_OPTIONS: Array<{
|
||||
value: IndicatorType;
|
||||
text: string;
|
||||
|
@ -205,6 +207,13 @@ export const SYNTHETICS_AVAILABILITY_DEFAULT_VALUES: SyntheticsAvailabilityIndic
|
|||
},
|
||||
};
|
||||
|
||||
export const SETTINGS_DEFAULT_VALUES = {
|
||||
frequency: 1,
|
||||
preventInitialBackfill: false,
|
||||
syncDelay: 1,
|
||||
syncField: null,
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
|
@ -219,9 +228,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOForm = {
|
|||
target: 99,
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
settings: {
|
||||
preventInitialBackfill: false,
|
||||
},
|
||||
settings: SETTINGS_DEFAULT_VALUES,
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOForm = {
|
||||
|
@ -238,9 +245,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOForm = {
|
|||
target: 99,
|
||||
},
|
||||
groupBy: ALL_VALUE,
|
||||
settings: {
|
||||
preventInitialBackfill: false,
|
||||
},
|
||||
settings: SETTINGS_DEFAULT_VALUES,
|
||||
};
|
||||
|
||||
export const SLO_EDIT_FORM_DEFAULT_VALUES_SYNTHETICS_AVAILABILITY: CreateSLOForm = {
|
||||
|
@ -257,9 +262,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_SYNTHETICS_AVAILABILITY: CreateSLOForm
|
|||
target: 99,
|
||||
},
|
||||
groupBy: SYNTHETICS_DEFAULT_GROUPINGS,
|
||||
settings: {
|
||||
preventInitialBackfill: false,
|
||||
},
|
||||
settings: SETTINGS_DEFAULT_VALUES,
|
||||
};
|
||||
|
||||
export const COMPARATOR_GT = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.gtLabel', {
|
||||
|
|
|
@ -26,7 +26,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -74,7 +77,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -104,7 +110,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -146,7 +155,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -178,7 +190,10 @@ Object {
|
|||
"timesliceWindow": "2",
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -208,7 +223,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -218,6 +236,105 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Transform partial URL state into form state settings handles optional 'syncField' URL state 1`] = `
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "",
|
||||
"groupBy": "*",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "",
|
||||
"good": "",
|
||||
"index": "",
|
||||
"timestampField": "",
|
||||
"total": "",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "",
|
||||
"objective": Object {
|
||||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": "override-field",
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
"duration": "30d",
|
||||
"type": "rolling",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transform partial URL state into form state settings handles partial 'settings' URL state 1`] = `
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "",
|
||||
"groupBy": "*",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "",
|
||||
"good": "",
|
||||
"index": "",
|
||||
"timestampField": "",
|
||||
"total": "",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "",
|
||||
"objective": Object {
|
||||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 12,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
"duration": "30d",
|
||||
"type": "rolling",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transform partial URL state into form state settings handles the 'settings' URL state 1`] = `
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
"description": "",
|
||||
"groupBy": "*",
|
||||
"indicator": Object {
|
||||
"params": Object {
|
||||
"filter": "",
|
||||
"good": "",
|
||||
"index": "",
|
||||
"timestampField": "",
|
||||
"total": "",
|
||||
},
|
||||
"type": "sli.kql.custom",
|
||||
},
|
||||
"name": "",
|
||||
"objective": Object {
|
||||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": true,
|
||||
"syncDelay": 180,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
"duration": "30d",
|
||||
"type": "rolling",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transform partial URL state into form state with 'indicator' in URL state handles partial APM Availability state 1`] = `
|
||||
Object {
|
||||
"budgetingMethod": "occurrences",
|
||||
|
@ -239,7 +356,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -271,7 +391,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -301,7 +424,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
@ -331,7 +457,10 @@ Object {
|
|||
"target": 99,
|
||||
},
|
||||
"settings": Object {
|
||||
"frequency": 1,
|
||||
"preventInitialBackfill": false,
|
||||
"syncDelay": 1,
|
||||
"syncField": null,
|
||||
},
|
||||
"tags": Array [],
|
||||
"timeWindow": Object {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { getGroupByCardinalityFilters } from '../components/synthetics_availability/synthetics_availability_indicator_type_form';
|
||||
import { getGroupByCardinalityFilters } from '../components/indicator_section/synthetics_availability/synthetics_availability_indicator_type_form';
|
||||
import { formatAllFilters } from './format_filters';
|
||||
|
||||
describe('formatAllFilters', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformPartialUrlStateToFormState as transform } from './process_slo_form_values';
|
||||
import { transformPartialSLOStateToFormState as transform } from './process_slo_form_values';
|
||||
|
||||
describe('Transform partial URL state into form state', () => {
|
||||
describe("with 'indicator' in URL state", () => {
|
||||
|
@ -121,4 +121,20 @@ describe('Transform partial URL state into form state', () => {
|
|||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('settings', () => {
|
||||
it("handles the 'settings' URL state", () => {
|
||||
expect(
|
||||
transform({ settings: { preventInitialBackfill: true, syncDelay: '3h' } })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("handles partial 'settings' URL state", () => {
|
||||
expect(transform({ settings: { syncDelay: '12m' } })).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("handles optional 'syncField' URL state", () => {
|
||||
expect(transform({ settings: { syncField: 'override-field' } })).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,13 +9,14 @@ import { CreateSLOInput, GetSLOResponse, Indicator, UpdateSLOInput } from '@kbn/
|
|||
import { assertNever } from '@kbn/std';
|
||||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { toDuration } from '../../../utils/slo/duration';
|
||||
import { toDuration, toMinutes } from '../../../utils/slo/duration';
|
||||
import {
|
||||
APM_AVAILABILITY_DEFAULT_VALUES,
|
||||
APM_LATENCY_DEFAULT_VALUES,
|
||||
CUSTOM_KQL_DEFAULT_VALUES,
|
||||
CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
HISTOGRAM_DEFAULT_VALUES,
|
||||
SETTINGS_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES_SYNTHETICS_AVAILABILITY,
|
||||
SYNTHETICS_AVAILABILITY_DEFAULT_VALUES,
|
||||
|
@ -52,6 +53,13 @@ export function transformSloResponseToCreateSloForm(
|
|||
tags: values.tags,
|
||||
settings: {
|
||||
preventInitialBackfill: values.settings?.preventInitialBackfill ?? false,
|
||||
syncDelay: values.settings?.syncDelay
|
||||
? toMinutes(toDuration(values.settings.syncDelay))
|
||||
: SETTINGS_DEFAULT_VALUES.syncDelay,
|
||||
frequency: values.settings?.frequency
|
||||
? toMinutes(toDuration(values.settings.frequency))
|
||||
: SETTINGS_DEFAULT_VALUES.frequency,
|
||||
syncField: values.settings?.syncField ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -80,7 +88,10 @@ export function transformCreateSLOFormToCreateSLOInput(values: CreateSLOForm): C
|
|||
tags: values.tags,
|
||||
groupBy: [values.groupBy].flat(),
|
||||
settings: {
|
||||
preventInitialBackfill: values.settings?.preventInitialBackfill ?? false,
|
||||
preventInitialBackfill: values.settings.preventInitialBackfill,
|
||||
syncDelay: `${values.settings.syncDelay ?? SETTINGS_DEFAULT_VALUES.syncDelay}m`,
|
||||
frequency: `${values.settings.frequency ?? SETTINGS_DEFAULT_VALUES.frequency}m`,
|
||||
syncField: values.settings.syncField,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -109,7 +120,10 @@ export function transformValuesToUpdateSLOInput(values: CreateSLOForm): UpdateSL
|
|||
tags: values.tags,
|
||||
groupBy: [values.groupBy].flat(),
|
||||
settings: {
|
||||
preventInitialBackfill: values.settings?.preventInitialBackfill ?? false,
|
||||
preventInitialBackfill: values.settings.preventInitialBackfill,
|
||||
syncDelay: `${values.settings.syncDelay ?? SETTINGS_DEFAULT_VALUES.syncDelay}m`,
|
||||
frequency: `${values.settings.frequency ?? SETTINGS_DEFAULT_VALUES.frequency}m`,
|
||||
syncField: values.settings.syncField,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -165,7 +179,7 @@ function transformPartialIndicatorState(
|
|||
}
|
||||
}
|
||||
|
||||
export function transformPartialUrlStateToFormState(
|
||||
export function transformPartialSLOStateToFormState(
|
||||
values: RecursivePartial<CreateSLOInput>
|
||||
): CreateSLOForm {
|
||||
let state: CreateSLOForm;
|
||||
|
@ -189,8 +203,8 @@ export function transformPartialUrlStateToFormState(
|
|||
if (values.description) {
|
||||
state.description = values.description;
|
||||
}
|
||||
if (!!values.tags) {
|
||||
state.tags = values.tags as string[];
|
||||
if (values.tags) {
|
||||
state.tags = [values.tags].flat().filter((tag) => !!tag) as string[];
|
||||
}
|
||||
|
||||
if (values.objective) {
|
||||
|
@ -220,8 +234,19 @@ export function transformPartialUrlStateToFormState(
|
|||
state.timeWindow = { duration: values.timeWindow.duration, type: values.timeWindow.type };
|
||||
}
|
||||
|
||||
if (!!values.settings?.preventInitialBackfill) {
|
||||
state.settings = { preventInitialBackfill: values.settings.preventInitialBackfill };
|
||||
if (!!values.settings) {
|
||||
if (values.settings.preventInitialBackfill) {
|
||||
state.settings.preventInitialBackfill = values.settings.preventInitialBackfill;
|
||||
}
|
||||
if (values.settings.syncDelay) {
|
||||
state.settings.syncDelay = toMinutes(toDuration(values.settings.syncDelay));
|
||||
}
|
||||
if (values.settings.frequency) {
|
||||
state.settings.frequency = toMinutes(toDuration(values.settings.frequency));
|
||||
}
|
||||
if (values.settings.syncField) {
|
||||
state.settings.syncField = values.settings.syncField;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { CreateSLOInput } from '@kbn/slo-schema';
|
|||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMemo } from 'react';
|
||||
import { transformPartialUrlStateToFormState } from '../helpers/process_slo_form_values';
|
||||
import { transformPartialSLOStateToFormState } from '../helpers/process_slo_form_values';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
export function useParseUrlState(): CreateSLOForm | undefined {
|
||||
|
@ -25,6 +25,6 @@ export function useParseUrlState(): CreateSLOForm | undefined {
|
|||
|
||||
const urlState = urlStateStorage.get<RecursivePartial<CreateSLOInput>>('_a');
|
||||
|
||||
return !!urlState ? transformPartialUrlStateToFormState(urlState) : undefined;
|
||||
return !!urlState ? transformPartialSLOStateToFormState(urlState) : undefined;
|
||||
}, [history]);
|
||||
}
|
||||
|
|
|
@ -220,8 +220,10 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
|
|||
'objective.target',
|
||||
'objective.timesliceTarget',
|
||||
'objective.timesliceWindow',
|
||||
'settings.syncDelay',
|
||||
'settings.frequency',
|
||||
] as const
|
||||
).every((field) => getFieldState(field).error === undefined);
|
||||
).every((field) => !getFieldState(field).invalid);
|
||||
|
||||
const isDescriptionSectionValid =
|
||||
!getFieldState('name').invalid &&
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
CUSTOM_METRIC_DEFAULT_VALUES,
|
||||
HISTOGRAM_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
TIMESLICE_METRIC_DEFAULT_VALUES,
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES_SYNTHETICS_AVAILABILITY,
|
||||
TIMESLICE_METRIC_DEFAULT_VALUES,
|
||||
} from '../constants';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
|
||||
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import { OutPortal, createHtmlPortalNode } from 'react-reverse-portal';
|
||||
import { SloEditForm } from '../components/slo_edit_form';
|
||||
import { CreateSLOForm } from '../types';
|
||||
|
||||
export const sloEditFormFooterPortal = createHtmlPortalNode();
|
||||
|
||||
|
@ -22,7 +21,7 @@ export default function SloAddFormFlyout({
|
|||
initialValues,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
initialValues?: RecursivePartial<CreateSLOForm>;
|
||||
initialValues?: RecursivePartial<CreateSLOInput>;
|
||||
}) {
|
||||
return (
|
||||
<EuiFlyout
|
||||
|
@ -40,29 +39,7 @@ export default function SloAddFormFlyout({
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SloEditForm
|
||||
onSave={onClose}
|
||||
initialValues={
|
||||
initialValues
|
||||
? merge(
|
||||
{
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
},
|
||||
objective: {
|
||||
target: 99,
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '30d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
},
|
||||
{ ...initialValues }
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<SloEditForm onSave={onClose} initialValues={initialValues} />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<OutPortal node={sloEditFormFooterPortal} />
|
||||
|
|
|
@ -423,11 +423,11 @@ describe('SLO Edit Page', () => {
|
|||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
history.push(
|
||||
'/slos/123/edit?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))'
|
||||
'/slos/edit/123?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))'
|
||||
);
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: '/slos/123/edit', search: '', state: '', hash: '' });
|
||||
.mockReturnValue({ pathname: '/slos/edit/123', search: '', state: '', hash: '' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, data: slo });
|
||||
|
||||
|
@ -463,8 +463,7 @@ describe('SLO Edit Page', () => {
|
|||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: '/slos/123/edit', search: '', state: '', hash: '' });
|
||||
|
||||
.mockReturnValue({ pathname: '/slos/edit/123', search: '', state: '', hash: '' });
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, data: slo });
|
||||
|
||||
const { getByTestId } = render(<SloEditPage />);
|
||||
|
|
|
@ -12,10 +12,10 @@ import { useParams } from 'react-router-dom';
|
|||
import { paths } from '../../../common/locators/paths';
|
||||
import { HeaderMenu } from '../../components/header_menu/header_menu';
|
||||
import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { usePermissions } from '../../hooks/use_permissions';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { SloEditForm } from './components/slo_edit_form';
|
||||
|
||||
export function SloEditPage() {
|
||||
|
|
|
@ -25,5 +25,8 @@ export interface CreateSLOForm<IndicatorType = Indicator> {
|
|||
groupBy: string[] | string;
|
||||
settings: {
|
||||
preventInitialBackfill: boolean;
|
||||
syncDelay: number; // in minutes
|
||||
frequency: number; // in minutes
|
||||
syncField: string | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('remote SLO URLs Utils', () => {
|
|||
`"https://cloud.elast.co/app/slos/edit/fixed-id"`
|
||||
);
|
||||
expect(createRemoteSloCloneUrl(remoteSlo)).toMatchInlineSnapshot(
|
||||
`"https://cloud.elast.co/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,preventInitialBackfill:!f,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"`
|
||||
`"https://cloud.elast.co/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(dataViewId:some-data-view-id,filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,preventInitialBackfill:!f,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -71,7 +71,7 @@ describe('remote SLO URLs Utils', () => {
|
|||
`"https://cloud.elast.co/s/my-custom-space/app/slos/edit/fixed-id"`
|
||||
);
|
||||
expect(createRemoteSloCloneUrl(remoteSlo, 'my-custom-space')).toMatchInlineSnapshot(
|
||||
`"https://cloud.elast.co/s/my-custom-space/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,preventInitialBackfill:!f,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"`
|
||||
`"https://cloud.elast.co/s/my-custom-space/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(dataViewId:some-data-view-id,filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,preventInitialBackfill:!f,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typ
|
|||
import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server';
|
||||
import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { merge } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
SLO_MODEL_VERSION,
|
||||
|
@ -46,8 +47,10 @@ export class CreateSLO {
|
|||
const slo = this.toSLO(params);
|
||||
validateSLO(slo);
|
||||
|
||||
await this.assertSLOInexistant(slo);
|
||||
await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient);
|
||||
await Promise.all([
|
||||
this.assertSLOInexistant(slo),
|
||||
assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient),
|
||||
]);
|
||||
|
||||
const rollbackOperations = [];
|
||||
const createPromise = this.repository.create(slo);
|
||||
|
@ -201,11 +204,14 @@ export class CreateSLO {
|
|||
return {
|
||||
...params,
|
||||
id: params.id ?? uuidv4(),
|
||||
settings: {
|
||||
syncDelay: params.settings?.syncDelay ?? new Duration(1, DurationUnit.Minute),
|
||||
frequency: params.settings?.frequency ?? new Duration(1, DurationUnit.Minute),
|
||||
preventInitialBackfill: params.settings?.preventInitialBackfill ?? false,
|
||||
},
|
||||
settings: merge(
|
||||
{
|
||||
syncDelay: new Duration(1, DurationUnit.Minute),
|
||||
frequency: new Duration(1, DurationUnit.Minute),
|
||||
preventInitialBackfill: false,
|
||||
},
|
||||
params.settings
|
||||
),
|
||||
revision: params.revision ?? 1,
|
||||
enabled: true,
|
||||
tags: params.tags ?? [],
|
||||
|
|
|
@ -9,6 +9,7 @@ import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
|||
import { Logger } from '@kbn/core/server';
|
||||
import { ALL_VALUE, Paginated, Pagination, sloDefinitionSchema } from '@kbn/slo-schema';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { merge } from 'lodash';
|
||||
import { SLO_MODEL_VERSION } from '../../common/constants';
|
||||
import { SLODefinition, StoredSLODefinition } from '../domain/models';
|
||||
import { SLONotFound } from '../errors';
|
||||
|
@ -155,10 +156,10 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
|
|||
// We would need to call the _reset api on this SLO.
|
||||
version: storedSLO.version ?? 1,
|
||||
// settings.preventInitialBackfill was added in 8.15.0
|
||||
settings: {
|
||||
...storedSLO.settings,
|
||||
preventInitialBackfill: storedSLO.settings?.preventInitialBackfill ?? false,
|
||||
},
|
||||
settings: merge(
|
||||
{ preventInitialBackfill: false, syncDelay: '1m', frequency: '1m' },
|
||||
storedSLO.settings
|
||||
),
|
||||
});
|
||||
|
||||
if (isLeft(result)) {
|
||||
|
|
|
@ -63,3 +63,11 @@ Object {
|
|||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transform Generator settings builds the transform settings 1`] = `
|
||||
Object {
|
||||
"frequency": "2m",
|
||||
"sync_delay": "10m",
|
||||
"sync_field": "my_timestamp_sync_field",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -42,7 +42,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator
|
|||
this.buildDestination(slo),
|
||||
this.buildGroupBy(slo, slo.indicator),
|
||||
this.buildAggregations(slo, slo.indicator),
|
||||
this.buildSettings(slo),
|
||||
this.buildSettings(slo, '@timestamp'),
|
||||
slo
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato
|
|||
this.buildDestination(slo),
|
||||
this.buildGroupBy(slo, slo.indicator),
|
||||
this.buildAggregations(slo),
|
||||
this.buildSettings(slo),
|
||||
this.buildSettings(slo, '@timestamp'),
|
||||
slo
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Duration, DurationUnit } from '../../domain/models';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from '../fixtures/slo';
|
||||
import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate';
|
||||
import { dataViewsService } from '@kbn/data-views-plugin/server/mocks';
|
||||
|
@ -45,4 +46,46 @@ describe('Transform Generator', () => {
|
|||
expect(runtimeMappings).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings', () => {
|
||||
const defaultSettings = {
|
||||
syncDelay: new Duration(10, DurationUnit.Minute),
|
||||
frequency: new Duration(2, DurationUnit.Minute),
|
||||
preventInitialBackfill: true,
|
||||
};
|
||||
|
||||
it('builds the transform settings', async () => {
|
||||
const slo = createSLO({
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
syncField: 'my_timestamp_sync_field',
|
||||
},
|
||||
});
|
||||
const settings = generator.buildSettings(slo);
|
||||
expect(settings).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('builds the transform settings using the provided settings.syncField', async () => {
|
||||
const slo = createSLO({
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
syncField: 'my_timestamp_sync_field',
|
||||
},
|
||||
});
|
||||
const settings = generator.buildSettings(slo, '@timestamp');
|
||||
expect(settings.sync_field).toEqual('my_timestamp_sync_field');
|
||||
});
|
||||
|
||||
it('builds the transform settings using provided fallback when no settings.syncField is configured', async () => {
|
||||
const slo = createSLO({ settings: defaultSettings });
|
||||
const settings = generator.buildSettings(slo, '@timestamp2');
|
||||
expect(settings.sync_field).toEqual('@timestamp2');
|
||||
});
|
||||
|
||||
it("builds the transform settings using '@timestamp' default fallback when no settings.syncField is configured", async () => {
|
||||
const slo = createSLO({ settings: defaultSettings });
|
||||
const settings = generator.buildSettings(slo);
|
||||
expect(settings.sync_field).toEqual('@timestamp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -88,8 +88,9 @@ export abstract class TransformGenerator {
|
|||
): TransformSettings {
|
||||
return {
|
||||
frequency: slo.settings.frequency.format(),
|
||||
sync_field: sourceIndexTimestampField, // timestamp field defined in the source index
|
||||
sync_delay: slo.settings.syncDelay.format(),
|
||||
// 8.17: use settings.syncField if truthy or default to sourceIndexTimestampField which is the indicator timestampField
|
||||
sync_field: !!slo.settings.syncField ? slo.settings.syncField : sourceIndexTimestampField,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,9 +43,10 @@ export class UpdateSLO {
|
|||
|
||||
public async execute(sloId: string, params: UpdateSLOParams): Promise<UpdateSLOResponse> {
|
||||
const originalSlo = await this.repository.findById(sloId);
|
||||
let updatedSlo: SLODefinition = Object.assign({}, originalSlo, params, {
|
||||
let updatedSlo: SLODefinition = Object.assign({}, originalSlo, {
|
||||
...params,
|
||||
groupBy: !!params.groupBy ? params.groupBy : originalSlo.groupBy,
|
||||
settings: mergePartialSettings(originalSlo.settings, params.settings),
|
||||
settings: Object.assign({}, originalSlo.settings, params.settings),
|
||||
});
|
||||
|
||||
if (isEqual(originalSlo, updatedSlo)) {
|
||||
|
@ -263,13 +264,3 @@ export class UpdateSLO {
|
|||
return updateSLOResponseSchema.encode(slo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings are merged by overwriting the original settings with the optional new partial settings.
|
||||
*/
|
||||
function mergePartialSettings(
|
||||
originalSettings: SLODefinition['settings'],
|
||||
newPartialSettings: UpdateSLOParams['settings']
|
||||
) {
|
||||
return Object.assign({}, originalSettings, newPartialSettings);
|
||||
}
|
||||
|
|
|
@ -41,10 +41,6 @@ export function useCreateSLO({
|
|||
tags: [],
|
||||
},
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.99,
|
||||
},
|
||||
tags: tags || [],
|
||||
groupBy: ['monitor.name', 'observer.geo.name', 'monitor.id'],
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue