mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
chore(slo): UI and tests improvements (#152959)
This commit is contained in:
parent
fc9a63118c
commit
57bbdd658d
23 changed files with 370 additions and 278 deletions
|
@ -5,13 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
asDecimal,
|
||||
asInteger,
|
||||
asPercent,
|
||||
asPercentWithTwoDecimals,
|
||||
asDecimalOrInteger,
|
||||
} from './formatters';
|
||||
import { asDecimal, asInteger, asPercent, asDecimalOrInteger } from './formatters';
|
||||
|
||||
describe('formatters', () => {
|
||||
describe('asDecimal', () => {
|
||||
|
@ -95,33 +89,6 @@ describe('formatters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('asPercentWithTwoDecimals', () => {
|
||||
it('formats as integer when number is above 10', () => {
|
||||
expect(asPercentWithTwoDecimals(3725, 10000, 'n/a')).toEqual('37.25%');
|
||||
});
|
||||
|
||||
it('adds a decimal when value is below 10', () => {
|
||||
expect(asPercentWithTwoDecimals(0.092, 1)).toEqual('9.20%');
|
||||
});
|
||||
|
||||
it('formats when numerator is 0', () => {
|
||||
expect(asPercentWithTwoDecimals(0, 1, 'n/a')).toEqual('0%');
|
||||
});
|
||||
|
||||
it('returns fallback when denominator is undefined', () => {
|
||||
expect(asPercentWithTwoDecimals(3725, undefined, 'n/a')).toEqual('n/a');
|
||||
});
|
||||
|
||||
it('returns fallback when denominator is 0 ', () => {
|
||||
expect(asPercentWithTwoDecimals(3725, 0, 'n/a')).toEqual('n/a');
|
||||
});
|
||||
|
||||
it('returns fallback when numerator or denominator is NaN', () => {
|
||||
expect(asPercentWithTwoDecimals(3725, NaN, 'n/a')).toEqual('n/a');
|
||||
expect(asPercentWithTwoDecimals(NaN, 10000, 'n/a')).toEqual('n/a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('asDecimalOrInteger', () => {
|
||||
it('formats as integer when number equals to 0 ', () => {
|
||||
expect(asDecimalOrInteger(0)).toEqual('0');
|
||||
|
|
|
@ -47,27 +47,6 @@ export function asPercent(
|
|||
return numeral(decimal).format('0.0%');
|
||||
}
|
||||
|
||||
export function asPercentWithTwoDecimals(
|
||||
numerator: Maybe<number>,
|
||||
denominator: number | undefined,
|
||||
fallbackResult = NOT_AVAILABLE_LABEL
|
||||
) {
|
||||
if (!denominator || !isFiniteNumber(numerator)) {
|
||||
return fallbackResult;
|
||||
}
|
||||
|
||||
const decimal = numerator / denominator;
|
||||
|
||||
// 33.2 => 33.20%
|
||||
// 3.32 => 3.32%
|
||||
// 0 => 0%
|
||||
if (String(Math.abs(decimal)).split('.').at(1)?.length === 2 || decimal === 0 || decimal === 1) {
|
||||
return numeral(decimal).format('0%');
|
||||
}
|
||||
|
||||
return numeral(decimal).format('0.00%');
|
||||
}
|
||||
|
||||
export type AsPercent = typeof asPercent;
|
||||
|
||||
export function asDecimalOrInteger(value: number) {
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toDurationLabel } from '../../../utils/slo/labels';
|
||||
import { ChartData } from '../../../typings/slo';
|
||||
import { toHighPrecisionPercentage } from '../helpers/number';
|
||||
import { WideChart } from './wide_chart';
|
||||
|
||||
export interface Props {
|
||||
|
@ -21,10 +23,13 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
|
||||
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder>
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="errorBudgetChartPanel">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
|
@ -40,7 +45,7 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
|
|||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.duration', {
|
||||
defaultMessage: 'Last {duration}',
|
||||
values: { duration: slo.timeWindow.duration },
|
||||
values: { duration: toDurationLabel(slo.timeWindow.duration) },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -50,7 +55,7 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleColor={isSloFailed ? 'danger' : 'success'}
|
||||
title={`${toHighPrecisionPercentage(slo.summary.errorBudget.remaining)}%`}
|
||||
title={numeral(slo.summary.errorBudget.remaining).format(percentFormat)}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.remaining',
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiPanel } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import { toBudgetingMethodLabel, toIndicatorTypeLabel } from '../../../utils/slo/labels';
|
||||
import { toDurationLabel } from '../../../utils/slo/labels';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toHighPrecisionPercentage } from '../helpers/number';
|
||||
import { OverviewItem } from './overview_item';
|
||||
|
||||
export interface Props {
|
||||
|
@ -23,10 +24,11 @@ export interface Props {
|
|||
export function Overview({ slo }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const dateFormat = uiSettings.get('dateFormat');
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
const hasNoData = slo.summary.status === 'NO_DATA';
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="none" color="transparent">
|
||||
<EuiPanel paddingSize="none" color="transparent" data-test-subj="overview">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="row" alignItems="flexStart">
|
||||
<OverviewItem
|
||||
|
@ -41,8 +43,8 @@ export function Overview({ slo }: Props) {
|
|||
{
|
||||
defaultMessage: '{value} (objective is {objective})',
|
||||
values: {
|
||||
value: hasNoData ? '-' : `${toHighPrecisionPercentage(slo.summary.sliValue)}%`,
|
||||
objective: `${toHighPrecisionPercentage(slo.objective.target)}%`,
|
||||
value: hasNoData ? '-' : numeral(slo.summary.sliValue).format(percentFormat),
|
||||
objective: numeral(slo.objective.target).format(percentFormat),
|
||||
},
|
||||
}
|
||||
)}
|
||||
|
@ -69,7 +71,7 @@ export function Overview({ slo }: Props) {
|
|||
defaultMessage: 'Budgeting method',
|
||||
}
|
||||
)}
|
||||
subtitle={toBudgetingMethod(slo.budgetingMethod)}
|
||||
subtitle={toBudgetingMethodLabel(slo.budgetingMethod)}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
@ -109,7 +111,7 @@ function toTimeWindowLabel(timeWindow: SLOWithSummaryResponse['timeWindow']): st
|
|||
return i18n.translate('xpack.observability.slo.sloDetails.overview.rollingTimeWindow', {
|
||||
defaultMessage: '{duration} rolling',
|
||||
values: {
|
||||
duration: timeWindow.duration,
|
||||
duration: toDurationLabel(timeWindow.duration),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -121,40 +123,3 @@ function toTimeWindowLabel(timeWindow: SLOWithSummaryResponse['timeWindow']): st
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toIndicatorTypeLabel(indicatorType: SLOWithSummaryResponse['indicator']['type']): string {
|
||||
switch (indicatorType) {
|
||||
case 'sli.kql.custom':
|
||||
return i18n.translate('xpack.observability.slo.sloDetails.overview.customKqlIndicator', {
|
||||
defaultMessage: 'Custom KQL',
|
||||
});
|
||||
|
||||
case 'sli.apm.transactionDuration':
|
||||
return i18n.translate('xpack.observability.slo.sloDetails.overview.apmLatencyIndicator', {
|
||||
defaultMessage: 'APM latency',
|
||||
});
|
||||
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
return i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.overview.apmAvailabilityIndicator',
|
||||
{
|
||||
defaultMessage: 'APM availability',
|
||||
}
|
||||
);
|
||||
default:
|
||||
assertNever(indicatorType);
|
||||
}
|
||||
}
|
||||
|
||||
function toBudgetingMethod(budgetingMethod: SLOWithSummaryResponse['budgetingMethod']): string {
|
||||
if (budgetingMethod === 'occurrences') {
|
||||
return i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.overview.occurrencesBudgetingMethod',
|
||||
{ defaultMessage: 'Occurrences' }
|
||||
);
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.observability.slo.sloDetails.overview.timeslicesBudgetingMethod', {
|
||||
defaultMessage: 'Timeslices',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toDurationLabel } from '../../../utils/slo/labels';
|
||||
import { ChartData } from '../../../typings/slo';
|
||||
import { toHighPrecisionPercentage } from '../helpers/number';
|
||||
import { WideChart } from './wide_chart';
|
||||
|
||||
export interface Props {
|
||||
|
@ -21,11 +23,14 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function SliChartPanel({ data, isLoading, slo }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
|
||||
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
|
||||
const hasNoData = slo.summary.status === 'NO_DATA';
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder>
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="sliChartPanel">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
|
@ -41,7 +46,7 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
|
|||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration', {
|
||||
defaultMessage: 'Last {duration}',
|
||||
values: { duration: slo.timeWindow.duration },
|
||||
values: { duration: toDurationLabel(slo.timeWindow.duration) },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -51,7 +56,7 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleColor={isSloFailed ? 'danger' : 'success'}
|
||||
title={hasNoData ? '-' : `${toHighPrecisionPercentage(slo.summary.sliValue)}%`}
|
||||
title={hasNoData ? '-' : numeral(slo.summary.sliValue).format(percentFormat)}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.sliHistoryChartPanel.current',
|
||||
|
@ -62,7 +67,7 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
title={`${toHighPrecisionPercentage(slo.objective.target)}%`}
|
||||
title={numeral(slo.objective.target).format(percentFormat)}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.sliHistoryChartPanel.objective',
|
||||
|
|
|
@ -17,11 +17,11 @@ import {
|
|||
} from '@elastic/charts';
|
||||
import React from 'react';
|
||||
import { EuiIcon, EuiLoadingChart, useEuiTheme } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ChartData } from '../../../typings';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toHighPrecisionPercentage } from '../helpers/number';
|
||||
|
||||
type ChartType = 'area' | 'line';
|
||||
type State = 'success' | 'error';
|
||||
|
@ -40,12 +40,13 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) {
|
|||
const baseTheme = charts.theme.useChartsBaseTheme();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const dateFormat = uiSettings.get('dateFormat');
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
|
||||
const color = state === 'error' ? euiTheme.colors.danger : euiTheme.colors.success;
|
||||
const ChartComponent = chart === 'area' ? AreaSeries : LineSeries;
|
||||
|
||||
if (isLoading) {
|
||||
return <EuiLoadingChart size="m" mono />;
|
||||
return <EuiLoadingChart size="m" mono data-test-subj="wideChartLoading" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -67,7 +68,7 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) {
|
|||
id="left"
|
||||
ticks={4}
|
||||
position={Position.Left}
|
||||
tickFormat={(d) => `${toHighPrecisionPercentage(d)}%`}
|
||||
tickFormat={(d) => numeral(d).format(percentFormat)}
|
||||
/>
|
||||
<ChartComponent
|
||||
color={color}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function toHighPrecisionPercentage(value: number): number {
|
||||
return Math.trunc(value * 100000) / 1000;
|
||||
}
|
|
@ -18,7 +18,10 @@ import { buildSlo } from '../../data/slo/slo';
|
|||
import { paths } from '../../config';
|
||||
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { historicalSummaryData } from '../../data/slo/historical_summary_data';
|
||||
import {
|
||||
HEALTHY_STEP_DOWN_ROLLING_SLO,
|
||||
historicalSummaryData,
|
||||
} from '../../data/slo/historical_summary_data';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { buildApmAvailabilityIndicator } from '../../data/slo/indicator';
|
||||
|
||||
|
@ -57,6 +60,7 @@ const mockKibana = () => {
|
|||
uiSettings: {
|
||||
get: (settings: string) => {
|
||||
if (settings === 'dateFormat') return 'YYYY-MM-DD';
|
||||
if (settings === 'format:percent:defaultPattern') return '0.0%';
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
@ -88,31 +92,78 @@ describe('SLO Details Page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
it('renders the not found page when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('nonexistent');
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
it('renders the PageNotFound when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('nonexistent');
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the loading spinner when fetching the SLO', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: true, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetailsLoading')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the SLO details page with loading charts when summary data is loading', async () => {
|
||||
const slo = buildSlo({ id: HEALTHY_STEP_DOWN_ROLLING_SLO });
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: true,
|
||||
sloHistoricalSummaryResponse: {},
|
||||
});
|
||||
|
||||
it('renders the loading spinner when fetching the SLO', async () => {
|
||||
const slo = buildSlo();
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('overview')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sliChartPanel')).toBeTruthy();
|
||||
expect(screen.queryByTestId('errorBudgetChartPanel')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('wideChartLoading').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders the SLO details page with the overview and chart panels', async () => {
|
||||
const slo = buildSlo({ id: HEALTHY_STEP_DOWN_ROLLING_SLO });
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('overview')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sliChartPanel')).toBeTruthy();
|
||||
expect(screen.queryByTestId('errorBudgetChartPanel')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('wideChartLoading').length).toBe(0);
|
||||
});
|
||||
|
||||
describe('when an APM SLO is loaded', () => {
|
||||
it("should render a 'Explore in APM' button", async () => {
|
||||
const slo = buildSlo({ indicator: buildApmAvailabilityIndicator() });
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: true, slo: undefined });
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetailsLoading')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetailsExploreInApmButton')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the SLO details page', async () => {
|
||||
describe('when an Custom KQL SLO is loaded', () => {
|
||||
it("should not render a 'Explore in APM' button", async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
@ -120,33 +171,7 @@ describe('SLO Details Page', () => {
|
|||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when an APM SLO is loaded', () => {
|
||||
it('should render a Explore in APM button', async () => {
|
||||
const slo = buildSlo({ indicator: buildApmAvailabilityIndicator() });
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsExploreInApmButton')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an Custom KQL SLO is loaded', () => {
|
||||
it('should not render a Explore in APM button', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsExploreInApmButton')).toBeFalsy();
|
||||
});
|
||||
expect(screen.queryByTestId('sloDetailsExploreInApmButton')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BudgetingMethod, CreateSLOInput } from '@kbn/slo-schema';
|
||||
import {
|
||||
BUDGETING_METHOD_OCCURRENCES,
|
||||
BUDGETING_METHOD_TIMESLICES,
|
||||
INDICATOR_APM_AVAILABILITY,
|
||||
INDICATOR_APM_LATENCY,
|
||||
INDICATOR_CUSTOM_KQL,
|
||||
} from '../../utils/slo/labels';
|
||||
|
||||
export const SLI_OPTIONS: Array<{
|
||||
value: CreateSLOInput['indicator']['type'];
|
||||
|
@ -14,36 +21,26 @@ export const SLI_OPTIONS: Array<{
|
|||
}> = [
|
||||
{
|
||||
value: 'sli.kql.custom',
|
||||
text: i18n.translate('xpack.observability.slo.sliTypes.kqlCustomIndicator', {
|
||||
defaultMessage: 'KQL custom',
|
||||
}),
|
||||
text: INDICATOR_CUSTOM_KQL,
|
||||
},
|
||||
{
|
||||
value: 'sli.apm.transactionDuration',
|
||||
text: i18n.translate('xpack.observability.slo.sliTypes.apmLatencyIndicator', {
|
||||
defaultMessage: 'APM latency',
|
||||
}),
|
||||
text: INDICATOR_APM_LATENCY,
|
||||
},
|
||||
{
|
||||
value: 'sli.apm.transactionErrorRate',
|
||||
text: i18n.translate('xpack.observability.slo.sliTypes.apmAvailabilityIndicator', {
|
||||
defaultMessage: 'APM availability',
|
||||
}),
|
||||
text: INDICATOR_APM_AVAILABILITY,
|
||||
},
|
||||
];
|
||||
|
||||
export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: string }> = [
|
||||
{
|
||||
value: 'occurrences',
|
||||
text: i18n.translate('xpack.observability.slo.sloEdit.budgetingMethod.occurrences', {
|
||||
defaultMessage: 'Occurrences',
|
||||
}),
|
||||
text: BUDGETING_METHOD_OCCURRENCES,
|
||||
},
|
||||
{
|
||||
value: 'timeslices',
|
||||
text: i18n.translate('xpack.observability.slo.sloEdit.budgetingMethod.timeslices', {
|
||||
defaultMessage: 'Timeslices',
|
||||
}),
|
||||
text: BUDGETING_METHOD_TIMESLICES,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts';
|
|||
import { SloStatusBadge } from '../../../../components/slo/slo_status_badge';
|
||||
import { SloIndicatorTypeBadge } from './slo_indicator_type_badge';
|
||||
import { SloTimeWindowBadge } from './slo_time_window_badge';
|
||||
import { toAlertsPageQueryFilter } from '../../helpers/alerts_page_query_filter';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
|
@ -31,7 +32,9 @@ export function SloBadges({ slo, activeAlerts }: Props) {
|
|||
const handleClick = () => {
|
||||
if (activeAlerts) {
|
||||
navigateToUrl(
|
||||
`${basePath.prepend(paths.observability.alerts)}?_a=${toAlertsPageQuery(activeAlerts)}`
|
||||
`${basePath.prepend(paths.observability.alerts)}?_a=${toAlertsPageQueryFilter(
|
||||
activeAlerts
|
||||
)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -67,12 +70,3 @@ export function SloBadges({ slo, activeAlerts }: Props) {
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function toAlertsPageQuery(activeAlerts: ActiveAlerts): string {
|
||||
const kuery = activeAlerts.ruleIds
|
||||
.map((ruleId) => `kibana.alert.rule.uuid:"${activeAlerts.ruleIds[0]}"`)
|
||||
.join(' or ');
|
||||
|
||||
const query = `(kuery:'${kuery}',rangeFrom:now-15m,rangeTo:now,status:all)`;
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import { toIndicatorTypeLabel } from '../../../../utils/slo/labels';
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
@ -19,27 +19,8 @@ export function SloIndicatorTypeBadge({ slo }: Props) {
|
|||
return (
|
||||
<div>
|
||||
<EuiBadge color={euiLightVars.euiColorDisabled}>
|
||||
{toIndicatorLabel(slo.indicator.type)}
|
||||
{toIndicatorTypeLabel(slo.indicator.type)}
|
||||
</EuiBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function toIndicatorLabel(indicatorType: SLOWithSummaryResponse['indicator']['type']) {
|
||||
switch (indicatorType) {
|
||||
case 'sli.kql.custom':
|
||||
return i18n.translate('xpack.observability.slo.slo.indicator.customKql', {
|
||||
defaultMessage: 'KQL',
|
||||
});
|
||||
case 'sli.apm.transactionDuration':
|
||||
return i18n.translate('xpack.observability.slo.slo.indicator.apmLatency', {
|
||||
defaultMessage: 'Latency',
|
||||
});
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
return i18n.translate('xpack.observability.slo.slo.indicator.apmAvailability', {
|
||||
defaultMessage: 'Availability',
|
||||
});
|
||||
default:
|
||||
assertNever(indicatorType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ import { i18n } from '@kbn/i18n';
|
|||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { toMomentUnitOfTime } from '../../../../utils/slo/duration';
|
||||
import { toDurationLabel } from '../../../../utils/slo/labels';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
@ -20,7 +23,6 @@ export function SloTimeWindowBadge({ slo }: Props) {
|
|||
const duration = Number(slo.timeWindow.duration.slice(0, -1));
|
||||
const unit = slo.timeWindow.duration.slice(-1);
|
||||
if ('isRolling' in slo.timeWindow) {
|
||||
const label = toDurationLabel(duration, unit);
|
||||
return (
|
||||
<div>
|
||||
<EuiBadge
|
||||
|
@ -28,7 +30,7 @@ export function SloTimeWindowBadge({ slo }: Props) {
|
|||
iconType="editorItemAlignRight"
|
||||
iconSide="left"
|
||||
>
|
||||
{label}
|
||||
{toDurationLabel(slo.timeWindow.duration)}
|
||||
</EuiBadge>
|
||||
</div>
|
||||
);
|
||||
|
@ -61,48 +63,3 @@ export function SloTimeWindowBadge({ slo }: Props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function toDurationLabel(duration: number, durationUnit: string) {
|
||||
switch (durationUnit) {
|
||||
case 'd':
|
||||
return i18n.translate('xpack.observability.slo.slo.timeWindow.days', {
|
||||
defaultMessage: '{duration} days',
|
||||
values: { duration },
|
||||
});
|
||||
case 'w':
|
||||
return i18n.translate('xpack.observability.slo.slo.timeWindow.weeks', {
|
||||
defaultMessage: '{duration} weeks',
|
||||
values: { duration },
|
||||
});
|
||||
case 'M':
|
||||
return i18n.translate('xpack.observability.slo.slo.timeWindow.months', {
|
||||
defaultMessage: '{duration} months',
|
||||
values: { duration },
|
||||
});
|
||||
case 'Q':
|
||||
return i18n.translate('xpack.observability.slo.slo.timeWindow.quarterss', {
|
||||
defaultMessage: '{duration} quarters',
|
||||
values: { duration },
|
||||
});
|
||||
case 'Y':
|
||||
return i18n.translate('xpack.observability.slo.slo.timeWindow.years', {
|
||||
defaultMessage: '{duration} years',
|
||||
values: { duration },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toMomentUnitOfTime = (unit: string): moment.unitOfTime.Diff | undefined => {
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return 'days';
|
||||
case 'w':
|
||||
return 'weeks';
|
||||
case 'M':
|
||||
return 'months';
|
||||
case 'Q':
|
||||
return 'quarters';
|
||||
case 'Y':
|
||||
return 'years';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,6 +19,11 @@ import {
|
|||
import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
INDICATOR_APM_AVAILABILITY,
|
||||
INDICATOR_APM_LATENCY,
|
||||
INDICATOR_CUSTOM_KQL,
|
||||
} from '../../../utils/slo/labels';
|
||||
|
||||
export interface SloListSearchFilterSortBarProps {
|
||||
loading: boolean;
|
||||
|
@ -57,21 +62,15 @@ const SORT_OPTIONS: Array<Item<SortType>> = [
|
|||
|
||||
const INDICATOR_TYPE_OPTIONS: Array<Item<FilterType>> = [
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.indicatorTypeFilter.apmLatency', {
|
||||
defaultMessage: 'APM latency',
|
||||
}),
|
||||
label: INDICATOR_APM_LATENCY,
|
||||
type: 'sli.apm.transactionDuration',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.indicatorTypeFilter.apmAvailability', {
|
||||
defaultMessage: 'APM availability',
|
||||
}),
|
||||
label: INDICATOR_APM_AVAILABILITY,
|
||||
type: 'sli.apm.transactionErrorRate',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.indicatorTypeFilter.customKql', {
|
||||
defaultMessage: 'Custom KQL',
|
||||
}),
|
||||
label: INDICATOR_CUSTOM_KQL,
|
||||
type: 'sli.kql.custom',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -23,10 +23,10 @@ export interface Props {
|
|||
data: Data[];
|
||||
chart: ChartType;
|
||||
state: State;
|
||||
loading: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function SloSparkline({ chart, data, id, loading, state }: Props) {
|
||||
export function SloSparkline({ chart, data, id, isLoading, state }: Props) {
|
||||
const charts = useKibana().services.charts;
|
||||
const theme = charts.theme.useChartsTheme();
|
||||
const baseTheme = charts.theme.useChartsBaseTheme();
|
||||
|
@ -36,7 +36,7 @@ export function SloSparkline({ chart, data, id, loading, state }: Props) {
|
|||
const color = state === 'error' ? euiTheme.colors.danger : euiTheme.colors.success;
|
||||
const ChartComponent = chart === 'area' ? AreaSeries : LineSeries;
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
return <EuiLoadingChart size="m" mono />;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
import { asPercentWithTwoDecimals } from '../../../../common/utils/formatters';
|
||||
import { SloSparkline } from './slo_sparkline';
|
||||
|
||||
export interface Props {
|
||||
|
@ -21,6 +22,8 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function SloSummary({ slo, historicalSummary = [], historicalSummaryLoading }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
const isSloFailed = slo.summary.status === 'VIOLATED' || slo.summary.status === 'DEGRADING';
|
||||
const titleColor = isSloFailed ? 'danger' : '';
|
||||
const errorBudgetBurnDownData = formatHistoricalData(historicalSummary, 'error_budget_remaining');
|
||||
|
@ -28,18 +31,18 @@ export function SloSummary({ slo, historicalSummary = [], historicalSummaryLoadi
|
|||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="spaceBetween" gutterSize="xl">
|
||||
<EuiFlexItem grow={false} style={{ width: 200 }}>
|
||||
<EuiFlexItem grow={false} style={{ width: 220 }}>
|
||||
<EuiFlexGroup direction="row" responsive={false} gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false} style={{ width: 120 }}>
|
||||
<EuiFlexItem grow={false} style={{ width: 140 }}>
|
||||
<EuiStat
|
||||
description={i18n.translate('xpack.observability.slo.slo.stats.objective', {
|
||||
defaultMessage: '{objective} target',
|
||||
values: { objective: asPercentWithTwoDecimals(slo.objective.target, 1) },
|
||||
values: { objective: numeral(slo.objective.target).format(percentFormat) },
|
||||
})}
|
||||
title={
|
||||
slo.summary.status === 'NO_DATA'
|
||||
? NOT_AVAILABLE_LABEL
|
||||
: asPercentWithTwoDecimals(slo.summary.sliValue, 1)
|
||||
: numeral(slo.summary.sliValue).format(percentFormat)
|
||||
}
|
||||
titleColor={titleColor}
|
||||
titleSize="m"
|
||||
|
@ -52,7 +55,7 @@ export function SloSummary({ slo, historicalSummary = [], historicalSummaryLoadi
|
|||
id="sli_history"
|
||||
state={isSloFailed ? 'error' : 'success'}
|
||||
data={historicalSliData}
|
||||
loading={historicalSummaryLoading}
|
||||
isLoading={historicalSummaryLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -65,19 +68,19 @@ export function SloSummary({ slo, historicalSummary = [], historicalSummaryLoadi
|
|||
description={i18n.translate('xpack.observability.slo.slo.stats.budgetRemaining', {
|
||||
defaultMessage: 'Budget remaining',
|
||||
})}
|
||||
title={asPercentWithTwoDecimals(slo.summary.errorBudget.remaining, 1)}
|
||||
title={numeral(slo.summary.errorBudget.remaining).format(percentFormat)}
|
||||
titleColor={titleColor}
|
||||
titleSize="m"
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SloSparkline
|
||||
chart="area"
|
||||
id="error_budget_burn_down"
|
||||
state={isSloFailed ? 'error' : 'success'}
|
||||
data={errorBudgetBurnDownData}
|
||||
loading={historicalSummaryLoading}
|
||||
isLoading={historicalSummaryLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { toAlertsPageQueryFilter } from './alerts_page_query_filter';
|
||||
|
||||
describe('AlertsPageQueryFilter', () => {
|
||||
it('computes the query filter correctly', async () => {
|
||||
expect(
|
||||
toAlertsPageQueryFilter({ count: 2, ruleIds: ['rule-1', 'rule-2'] })
|
||||
).toMatchInlineSnapshot(
|
||||
`"(kuery:'kibana.alert.rule.uuid:\\"rule-1\\" or kibana.alert.rule.uuid:\\"rule-2\\"',rangeFrom:now-15m,rangeTo:now,status:all)"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
|
||||
export function toAlertsPageQueryFilter(activeAlerts: ActiveAlerts): string {
|
||||
const kuery = activeAlerts.ruleIds
|
||||
.map((ruleId) => `kibana.alert.rule.uuid:"${ruleId}"`)
|
||||
.join(' or ');
|
||||
|
||||
const query = `(kuery:'${kuery}',rangeFrom:now-15m,rangeTo:now,status:all)`;
|
||||
return query;
|
||||
}
|
|
@ -77,6 +77,13 @@ const mockKibana = () => {
|
|||
addError: mockAddError,
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
get: (settings: string) => {
|
||||
if (settings === 'dateFormat') return 'YYYY-MM-DD';
|
||||
if (settings === 'format:percent:defaultPattern') return '0.0%';
|
||||
return '';
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -75,6 +75,9 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
|
|||
if (setting === 'dateFormat') {
|
||||
return 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
}
|
||||
if (setting === 'format:percent:defaultPattern') {
|
||||
return '0,0.[000]%';
|
||||
}
|
||||
},
|
||||
},
|
||||
unifiedSearch: {},
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { Duration, DurationUnit } from '../../typings';
|
||||
|
||||
|
@ -33,3 +34,18 @@ export function toMinutes(duration: Duration) {
|
|||
|
||||
assertNever(duration.unit);
|
||||
}
|
||||
|
||||
export function toMomentUnitOfTime(unit: string): moment.unitOfTime.Diff | undefined {
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return 'days';
|
||||
case 'w':
|
||||
return 'weeks';
|
||||
case 'M':
|
||||
return 'months';
|
||||
case 'Q':
|
||||
return 'quarters';
|
||||
case 'Y':
|
||||
return 'years';
|
||||
}
|
||||
}
|
||||
|
|
115
x-pack/plugins/observability/public/utils/slo/labels.ts
Normal file
115
x-pack/plugins/observability/public/utils/slo/labels.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { toDuration } from './duration';
|
||||
|
||||
export const INDICATOR_CUSTOM_KQL = i18n.translate('xpack.observability.slo.indicators.customKql', {
|
||||
defaultMessage: 'Custom KQL',
|
||||
});
|
||||
|
||||
export const INDICATOR_APM_LATENCY = i18n.translate(
|
||||
'xpack.observability.slo.indicators.apmLatency',
|
||||
{ defaultMessage: 'APM latency' }
|
||||
);
|
||||
|
||||
export const INDICATOR_APM_AVAILABILITY = i18n.translate(
|
||||
'xpack.observability.slo.indicators.apmAvailability',
|
||||
{ defaultMessage: 'APM availability' }
|
||||
);
|
||||
|
||||
export function toIndicatorTypeLabel(
|
||||
indicatorType: SLOWithSummaryResponse['indicator']['type']
|
||||
): string {
|
||||
switch (indicatorType) {
|
||||
case 'sli.kql.custom':
|
||||
return INDICATOR_CUSTOM_KQL;
|
||||
|
||||
case 'sli.apm.transactionDuration':
|
||||
return INDICATOR_APM_LATENCY;
|
||||
|
||||
case 'sli.apm.transactionErrorRate':
|
||||
return INDICATOR_APM_AVAILABILITY;
|
||||
default:
|
||||
assertNever(indicatorType);
|
||||
}
|
||||
}
|
||||
|
||||
export const BUDGETING_METHOD_OCCURRENCES = i18n.translate(
|
||||
'xpack.observability.slo.budgetingMethod.occurrences',
|
||||
{
|
||||
defaultMessage: 'Occurrences',
|
||||
}
|
||||
);
|
||||
|
||||
export const BUDGETING_METHOD_TIMESLICES = i18n.translate(
|
||||
'xpack.observability.slo.budgetingMethod.timeslices',
|
||||
{
|
||||
defaultMessage: 'Timeslices',
|
||||
}
|
||||
);
|
||||
|
||||
export function toBudgetingMethodLabel(
|
||||
budgetingMethod: SLOWithSummaryResponse['budgetingMethod']
|
||||
): string {
|
||||
if (budgetingMethod === 'occurrences') {
|
||||
return BUDGETING_METHOD_OCCURRENCES;
|
||||
}
|
||||
|
||||
return BUDGETING_METHOD_TIMESLICES;
|
||||
}
|
||||
|
||||
export function toDurationLabel(durationStr: string): string {
|
||||
const duration = toDuration(durationStr);
|
||||
|
||||
switch (duration.unit) {
|
||||
case 'm':
|
||||
return i18n.translate('xpack.observability.slo.duration.minute', {
|
||||
defaultMessage: '{duration, plural, one {1 minute} other {# minutes}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'h':
|
||||
return i18n.translate('xpack.observability.slo.duration.hour', {
|
||||
defaultMessage: '{duration, plural, one {1 hour} other {# hours}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'd':
|
||||
return i18n.translate('xpack.observability.slo.duration.day', {
|
||||
defaultMessage: '{duration, plural, one {1 day} other {# days}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'w':
|
||||
return i18n.translate('xpack.observability.slo.duration.week', {
|
||||
defaultMessage: '{duration, plural, one {1 week} other {# weeks}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'M':
|
||||
return i18n.translate('xpack.observability.slo.duration.month', {
|
||||
defaultMessage: '{duration, plural, one {1 month} other {# months}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
case 'Y':
|
||||
return i18n.translate('xpack.observability.slo.duration.year', {
|
||||
defaultMessage: '{duration, plural, one {1 year} other {# years}}',
|
||||
values: {
|
||||
duration: duration.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -100,6 +100,50 @@ describe('BurnRateRuleExecutor', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('throws when the slo is not found', async () => {
|
||||
soClientMock.get.mockRejectedValue(new Error('NotFound'));
|
||||
const executor = getRuleExecutor();
|
||||
|
||||
await expect(
|
||||
executor({
|
||||
params: someRuleParams({ sloId: 'inexistent', burnRateThreshold: BURN_RATE_THRESHOLD }),
|
||||
startedAt: new Date(),
|
||||
services: servicesMock,
|
||||
executionId: 'irrelevant',
|
||||
logger: loggerMock,
|
||||
previousStartedAt: null,
|
||||
rule: {} as SanitizedRuleConfig,
|
||||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
})
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('returns early when the slo is disabled', async () => {
|
||||
const slo = createSLO({ objective: { target: 0.9 }, enabled: false });
|
||||
soClientMock.get.mockResolvedValue(aStoredSLO(slo));
|
||||
const executor = getRuleExecutor();
|
||||
|
||||
const result = await executor({
|
||||
params: someRuleParams({ sloId: slo.id, burnRateThreshold: BURN_RATE_THRESHOLD }),
|
||||
startedAt: new Date(),
|
||||
services: servicesMock,
|
||||
executionId: 'irrelevant',
|
||||
logger: loggerMock,
|
||||
previousStartedAt: null,
|
||||
rule: {} as SanitizedRuleConfig,
|
||||
spaceId: 'irrelevant',
|
||||
state: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
});
|
||||
|
||||
expect(esClientMock.search).not.toHaveBeenCalled();
|
||||
expect(alertWithLifecycleMock).not.toHaveBeenCalled();
|
||||
expect(alertFactoryMock.done).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ state: {} });
|
||||
});
|
||||
|
||||
it('does not schedule an alert when both windows burn rates are below the threshold', async () => {
|
||||
const slo = createSLO({ objective: { target: 0.9 } });
|
||||
soClientMock.get.mockResolvedValue(aStoredSLO(slo));
|
||||
|
|
|
@ -61,6 +61,10 @@ export const getRuleExecutor = (): LifecycleRuleExecutor<
|
|||
const summaryClient = new DefaultSLIClient(esClient.asCurrentUser);
|
||||
const slo = await sloRepository.findById(params.sloId);
|
||||
|
||||
if (!slo.enabled) {
|
||||
return { state: {} };
|
||||
}
|
||||
|
||||
const longWindowDuration = new Duration(
|
||||
params.longWindow.value,
|
||||
toDurationUnit(params.longWindow.unit)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue