feat(slo): Support for calendar aligned time window (#159949)

Resolves https://github.com/elastic/kibana/issues/159948

## 📝 Summary

This PR updates the SLO form to support the calendar aligned time
windows for both create and edit flow.
I've also moved the budgeting method selector down, so when selecting
"timeslices", the timeslices related inputs are shown next to it on the
same line.
 

| Screenshot | Screenshot |
|--------|--------| 
|
![screencapture-localhost-5601-kibana-app-observability-slos-edit-c1a51ac0-0eb0-11ee-8f7a-0da90ce06520-2023-06-19-11_53_05](9e786a17-ebce-43b5-b063-090fe89a1821)
|
![screencapture-localhost-5601-kibana-app-observability-slos-edit-c1a51ac0-0eb0-11ee-8f7a-0da90ce06520-2023-06-19-11_52_31](c3e7cab1-31c2-490b-b38f-6f7b01a3fc95)
|
This commit is contained in:
Kevin Delemme 2023-06-20 07:43:17 -04:00 committed by GitHub
parent 27df64c2bc
commit 3a34e3593d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 69 deletions

View file

@ -22,6 +22,7 @@ import {
summarySchema,
tagsSchema,
timeWindowSchema,
timeWindowTypeSchema,
} from '../schema';
const createSLOParamsSchema = t.type({
@ -166,6 +167,7 @@ type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.bod
type GetPreviewDataResponse = t.TypeOf<typeof getPreviewDataResponseSchema>;
type BudgetingMethod = t.TypeOf<typeof budgetingMethodSchema>;
type TimeWindow = t.TypeOf<typeof timeWindowTypeSchema>;
type Indicator = t.OutputOf<typeof indicatorSchema>;
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
@ -211,4 +213,5 @@ export type {
Indicator,
MetricCustomIndicator,
KQLCustomIndicator,
TimeWindow,
};

View file

@ -20,6 +20,10 @@ const calendarAlignedTimeWindowSchema = t.type({
type: calendarAlignedTimeWindowTypeSchema,
});
const timeWindowTypeSchema = t.union([
rollingTimeWindowTypeSchema,
calendarAlignedTimeWindowTypeSchema,
]);
const timeWindowSchema = t.union([rollingTimeWindowSchema, calendarAlignedTimeWindowSchema]);
export {
@ -28,4 +32,5 @@ export {
calendarAlignedTimeWindowSchema,
calendarAlignedTimeWindowTypeSchema,
timeWindowSchema,
timeWindowTypeSchema,
};

View file

@ -272,17 +272,16 @@ export function SloEditForm({ slo }: Props) {
})}
</EuiButton>
<EuiButton
color="ghost"
<EuiButtonEmpty
color="primary"
data-test-subj="sloFormCancelButton"
fill
disabled={isCreateSloLoading || isUpdateSloLoading}
onClick={() => navigateToUrl(basePath.prepend(paths.observability.slos))}
>
{i18n.translate('xpack.observability.slo.sloEdit.cancelButton', {
defaultMessage: 'Cancel',
})}
</EuiButton>
</EuiButtonEmpty>
<EuiButtonEmpty
color="primary"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import {
EuiFieldNumber,
EuiFlexGrid,
@ -22,13 +22,29 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { CreateSLOInput } from '@kbn/slo-schema';
import { SloEditFormObjectiveSectionTimeslices } from './slo_edit_form_objective_section_timeslices';
import { BUDGETING_METHOD_OPTIONS, TIMEWINDOW_OPTIONS } from '../constants';
import {
BUDGETING_METHOD_OPTIONS,
CALENDARALIGNED_TIMEWINDOW_OPTIONS,
ROLLING_TIMEWINDOW_OPTIONS,
TIMEWINDOW_TYPE_OPTIONS,
} from '../constants';
import { maxWidth } from './slo_edit_form';
export function SloEditFormObjectiveSection() {
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
const { control, watch, getFieldState, resetField } = useFormContext<CreateSLOInput>();
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
const timeWindowTypeSelect = useGeneratedHtmlId({ prefix: 'timeWindowTypeSelect' });
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
const timeWindowType = watch('timeWindow.type');
useEffect(() => {
resetField('timeWindow.duration', {
defaultValue:
timeWindowType === 'calendarAligned'
? CALENDARALIGNED_TIMEWINDOW_OPTIONS[1].value
: ROLLING_TIMEWINDOW_OPTIONS[1].value,
});
}, [timeWindowType, resetField]);
return (
<EuiPanel
@ -38,6 +54,88 @@ export function SloEditFormObjectiveSection() {
style={{ maxWidth }}
data-test-subj="sloEditFormObjectiveSection"
>
<EuiFlexGrid columns={3}>
<EuiFlexItem>
<EuiFormRow
label={
<span>
{i18n.translate('xpack.observability.slo.sloEdit.timeWindowType.label', {
defaultMessage: 'Time window',
})}{' '}
<EuiIconTip
content={i18n.translate(
'xpack.observability.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={String(field.value)}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={
<span>
{i18n.translate('xpack.observability.slo.sloEdit.timeWindowDuration.label', {
defaultMessage: 'Duration',
})}{' '}
<EuiIconTip
content={i18n.translate(
'xpack.observability.slo.sloEdit.timeWindowDuration.tooltip',
{
defaultMessage: 'The time window duration used to compute the SLO over.',
}
)}
position="top"
/>
</span>
}
>
<Controller
name="timeWindow.duration"
control={control}
shouldUnregister
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={String(field.value)}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="l" />
<EuiFlexGrid columns={3}>
<EuiFlexItem>
<EuiFormRow
@ -76,41 +174,14 @@ export function SloEditFormObjectiveSection() {
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={
<span>
{i18n.translate('xpack.observability.slo.sloEdit.timeWindow.label', {
defaultMessage: 'Time window',
})}{' '}
<EuiIconTip
content={i18n.translate('xpack.observability.slo.sloEdit.timeWindow.tooltip', {
defaultMessage:
'The rolling 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={TIMEWINDOW_OPTIONS}
value={String(field.value)}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
{watch('budgetingMethod') === 'timeslices' ? (
<SloEditFormObjectiveSectionTimeslices />
) : null}
</EuiFlexGrid>
<EuiSpacer size="l" />
<EuiFlexGrid columns={3}>
<EuiFlexItem>
<EuiFormRow
isInvalid={getFieldState('objective.target').invalid}
@ -153,13 +224,6 @@ export function SloEditFormObjectiveSection() {
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
{watch('budgetingMethod') === 'timeslices' ? (
<>
<EuiSpacer size="xl" />
<SloEditFormObjectiveSectionTimeslices />
</>
) : null}
</EuiPanel>
);
}

View file

@ -5,17 +5,17 @@
* 2.0.
*/
import React from 'react';
import { EuiFieldNumber, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Controller, useFormContext } from 'react-hook-form';
import type { CreateSLOInput } from '@kbn/slo-schema';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
export function SloEditFormObjectiveSectionTimeslices() {
const { control, getFieldState } = useFormContext<CreateSLOInput>();
return (
<EuiFlexGrid columns={3}>
<>
<EuiFlexItem>
<EuiFormRow
isInvalid={getFieldState('objective.timesliceTarget').invalid}
@ -98,6 +98,6 @@ export function SloEditFormObjectiveSectionTimeslices() {
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
</>
);
}

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { BudgetingMethod, CreateSLOInput } from '@kbn/slo-schema';
import { BudgetingMethod, CreateSLOInput, TimeWindow } from '@kbn/slo-schema';
import {
BUDGETING_METHOD_OCCURRENCES,
BUDGETING_METHOD_TIMESLICES,
@ -49,9 +49,39 @@ export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: str
},
];
export const TIMEWINDOW_OPTIONS = [90, 30, 7].map((number) => ({
export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindow; text: string }> = [
{
value: 'rolling',
text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.rolling', {
defaultMessage: 'Rolling',
}),
},
{
value: 'calendarAligned',
text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.calendarAligned', {
defaultMessage: 'Calendar aligned',
}),
},
];
export const CALENDARALIGNED_TIMEWINDOW_OPTIONS = [
{
value: '1w',
text: i18n.translate('xpack.observability.slo.sloEdit.calendarTimeWindow.weekly', {
defaultMessage: 'Weekly',
}),
},
{
value: '1M',
text: i18n.translate('xpack.observability.slo.sloEdit.calendarTimeWindow.monthly', {
defaultMessage: 'Monthly',
}),
},
];
export const ROLLING_TIMEWINDOW_OPTIONS = [90, 30, 7].map((number) => ({
value: `${number}d`,
text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.days', {
text: i18n.translate('xpack.observability.slo.sloEdit.rollingTimeWindow.days', {
defaultMessage: '{number} days',
values: { number },
}),
@ -71,8 +101,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOInput = {
},
},
timeWindow: {
duration:
TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
type: 'rolling',
},
tags: [],
@ -96,8 +125,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOInput = {
},
},
timeWindow: {
duration:
TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
type: 'rolling',
},
tags: [],

View file

@ -26635,7 +26635,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{value} (l'objectif est {objective})",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} en cours",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "Dernière(s) {duration}",
"xpack.observability.slo.sloEdit.timeWindow.days": "{number} jours",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " et inférieur à {bad}",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd} %)",
@ -26998,8 +26997,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "La cible d'intervalle de temps individuel utilisée pour déterminer si l'intervalle est bon ou mauvais.",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "Fenêtre d'intervalle de temps (en minutes)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "La taille de la fenêtre d'intervalle de temps utilisée pour évaluer les données.",
"xpack.observability.slo.sloEdit.timeWindow.label": "Fenêtre temporelle",
"xpack.observability.slo.sloEdit.timeWindow.tooltip": "La durée de la fenêtre temporelle glissante utilisée pour calculer le SLO.",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "Créer un nouveau SLO",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "Créer un SLO",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "Pour commencer, créez votre premier SLO.",

View file

@ -26617,7 +26617,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{objective}(目的は{value}",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration}ローリング",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "過去{duration}",
"xpack.observability.slo.sloEdit.timeWindow.days": "{number}日",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " {bad}未満",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)",
@ -26980,8 +26979,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "スライスが良好か問題があるかどうかを判断するために使用される、個別のタイムスライス目標。",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "タイムスライス期間(分)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "データを評価するために使用されるタイムスライス期間サイズ。",
"xpack.observability.slo.sloEdit.timeWindow.label": "時間枠",
"xpack.observability.slo.sloEdit.timeWindow.tooltip": "SLOを計算するために使用されるローリング時間枠期間。",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "新規SLOを作成",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "SLOの作成",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "開始するには、まずSLOを作成します。",

View file

@ -26615,7 +26615,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{value}(目标为 {objective}",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} 滚动",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "过去 {duration}",
"xpack.observability.slo.sloEdit.timeWindow.days": "{number} 天",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)",
@ -26978,8 +26977,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "用于确定切片是良好还是不良的单个时间片目标。",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "时间片窗口(分钟)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "用于评估接收的数据的时间片窗口大小。",
"xpack.observability.slo.sloEdit.timeWindow.label": "时间窗口",
"xpack.observability.slo.sloEdit.timeWindow.tooltip": "用于在其间计算 SLO 的滚动时间窗口持续时间。",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "创建新 SLO",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "创建 SLO",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "要开始使用,请创建您的首个 SLO。",