Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export function SloErrorBudget({
}

const hasGroupBy = slo.instanceId !== ALL_VALUE;
const lastErrorBudgetRemaining = sloHistoricalSummary?.data?.at(-1)?.errorBudget.remaining;

return (
<div data-shared-item="" ref={containerRef} style={{ width: '100%', padding: 10 }}>
<EuiFlexGroup direction="column" gutterSize="xs">
Expand Down Expand Up @@ -136,6 +138,7 @@ export function SloErrorBudget({
data={errorBudgetBurnDownData}
isLoading={historicalSummaryLoading}
slo={slo!}
lastErrorBudgetRemaining={lastErrorBudgetRemaining}
/>
</EuiFlexGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { EuiFlexItem } from '@elastic/eui';
import React, { type ReactNode } from 'react';
import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
import type { ChartData } from '../../../typings/slo';
import type { TimeBounds } from '../types';
import { WideChart } from './wide_chart';

type ChartType = 'area' | 'line';
type State = 'success' | 'error';

export interface BaseChartProps {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
onBrushed?: (timeBounds: TimeBounds) => void;
chartType: ChartType;
chartId: string;
metadata?: ReactNode;
}

export function BaseChart({
data,
isLoading,
slo,
chartType,
chartId,
metadata,
onBrushed,
}: BaseChartProps) {
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
const state: State = isSloFailed ? 'error' : 'success';

return (
<>
{metadata}

<EuiFlexItem>
<WideChart
chart={chartType}
id={chartId}
state={state}
data={data}
isLoading={isLoading}
onBrushed={onBrushed}
slo={slo}
/>
</EuiFlexItem>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useKibana } from '../../../hooks/use_kibana';
import type { ChartData } from '../../../typings/slo';
import { toDuration, toMinutes } from '../../../utils/slo/duration';
import type { TimeBounds } from '../types';
import { WideChart } from './wide_chart';
import { BaseChart } from './base_chart';

function formatTime(minutes: number) {
if (minutes > 59) {
Expand All @@ -35,73 +35,76 @@ export interface Props {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
hideMetadata?: boolean;
onBrushed?: (timeBounds: TimeBounds) => void;
lastErrorBudgetRemaining?: number;
}

export function ErrorBudgetChart({ data, isLoading, slo, hideMetadata = false, onBrushed }: Props) {
export function ErrorBudgetChart({
data,
isLoading,
slo,
onBrushed,
lastErrorBudgetRemaining,
}: Props) {
const { uiSettings } = useKibana().services;
const percentFormat = uiSettings.get('format:percent:defaultPattern');
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
let errorBudgetTimeRemainingFormatted;
if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') {
const totalSlices =
toMinutes(toDuration(slo.timeWindow.duration)) /
toMinutes(toDuration(slo.objective.timesliceWindow!));
const errorBudgetRemainingInMinute =
slo.summary.errorBudget.remaining * (slo.summary.errorBudget.initial * totalSlices);

errorBudgetTimeRemainingFormatted = formatTime(
errorBudgetRemainingInMinute >= 0 ? errorBudgetRemainingInMinute : 0
);
}
return (
<>
{!hideMetadata && (
<EuiFlexGroup direction="row" gutterSize="l" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={numeral(slo.summary.errorBudget.remaining).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.remaining', {
defaultMessage: 'Remaining',
})}
reverse
/>
</EuiFlexItem>
{errorBudgetTimeRemainingFormatted ? (
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={errorBudgetTimeRemainingFormatted}
titleSize="s"
description={i18n.translate(
'xpack.slo.sloDetails.errorBudgetChartPanel.remaining',
{
defaultMessage: 'Remaining',
}
)}
reverse
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
)}
const errorBudgetTimeRemainingFormatted = (() => {
if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') {
const totalSlices =
toMinutes(toDuration(slo.timeWindow.duration)) /
toMinutes(toDuration(slo.objective.timesliceWindow!));
const errorBudgetRemainingInMinute =
slo.summary.errorBudget.remaining * (slo.summary.errorBudget.initial * totalSlices);

return formatTime(errorBudgetRemainingInMinute >= 0 ? errorBudgetRemainingInMinute : 0);
}
return undefined;
})();

<EuiFlexItem>
<WideChart
chart="area"
id={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.chartTitle', {
defaultMessage: 'Error budget remaining',
const metadata = (
<EuiFlexGroup direction="row" gutterSize="l" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={
lastErrorBudgetRemaining ? numeral(lastErrorBudgetRemaining).format(percentFormat) : '-'
}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.remaining', {
defaultMessage: 'Remaining',
})}
state={isSloFailed ? 'error' : 'success'}
data={data}
isLoading={isLoading}
onBrushed={onBrushed}
slo={slo}
reverse
/>
</EuiFlexItem>
</>
{errorBudgetTimeRemainingFormatted ? (
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={errorBudgetTimeRemainingFormatted}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.remaining', {
defaultMessage: 'Remaining',
})}
reverse
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);

return (
<BaseChart
data={data}
isLoading={isLoading}
slo={slo}
onBrushed={onBrushed}
chartType="area"
chartId={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.chartTitle', {
defaultMessage: 'Error budget remaining',
})}
metadata={metadata}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ export interface Props {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
hideMetadata?: boolean;
onBrushed?: (timeBounds: TimeBounds) => void;
lastErrorBudgetRemaining?: number;
hideHeaderDurationLabel?: boolean;
}

export function ErrorBudgetChartPanel({
data,
isLoading,
slo,
hideMetadata = false,
onBrushed,
lastErrorBudgetRemaining,
hideHeaderDurationLabel = false,
}: Props) {
const [isMouseOver, setIsMouseOver] = useState(false);

Expand Down Expand Up @@ -88,17 +90,17 @@ export function ErrorBudgetChartPanel({
<EuiFlexGroup direction="column" gutterSize="l">
<ErrorBudgetHeader
slo={slo}
hideHeaderDurationLabel={hideHeaderDurationLabel}
isMouseOver={isMouseOver}
setDashboardAttachmentReady={setDashboardAttachmentReady}
hideMetadata={hideMetadata}
/>

<ErrorBudgetChart
slo={slo}
data={data}
isLoading={isLoading}
hideMetadata={hideMetadata}
onBrushed={onBrushed}
lastErrorBudgetRemaining={lastErrorBudgetRemaining}
/>
</EuiFlexGroup>
</EuiPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import { ErrorBudgetActions } from './error_budget_actions';
interface Props {
slo: SLOWithSummaryResponse;
hideTitle?: boolean;
hideHeaderDurationLabel?: boolean;
isMouseOver?: boolean;
setDashboardAttachmentReady?: (value: boolean) => void;
hideMetadata?: boolean;
}

export function ErrorBudgetHeader({
slo,
hideTitle = false,
hideMetadata = false,
hideHeaderDurationLabel = false,
isMouseOver,
setDashboardAttachmentReady,
}: Props) {
Expand Down Expand Up @@ -60,7 +60,7 @@ export function ErrorBudgetHeader({
)}
</EuiFlexGroup>
</EuiFlexItem>
{!hideMetadata && (
{!hideHeaderDurationLabel && (
<EuiFlexItem>
<EuiText color="subdued" size="s">
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
Expand Down
Loading