[SLOs] Add details page history tab (#181150)

## Summary

This will allow users to view data beyond default values in details page
chart, users will be able to select date range via picker to view
historical SLI, Burn rate or Good/Bad Events chart


48a6a029-ad36-4457-b195-4ab2d739fca0


<img width="1726" alt="image"
src="c8936443-5fe4-4925-9a9f-a22c847f32b7">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-05-01 13:03:41 +02:00 committed by GitHub
parent 7fc54940fb
commit 151aab5c15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 761 additions and 269 deletions

View file

@ -31,7 +31,13 @@ const fetchHistoricalSummaryParamsSchema = t.type({
groupBy: allOrAnyStringOrArray,
revision: t.number,
}),
t.partial({ remoteName: t.string }),
t.partial({
remoteName: t.string,
range: t.type({
from: t.string,
to: t.string,
}),
}),
])
),
}),

View file

@ -81,7 +81,10 @@ const groupSummarySchema = t.type({
noData: t.number,
});
const dateRangeSchema = t.type({ from: dateType, to: dateType });
const dateRangeSchema = t.type({
from: t.union([dateType, t.string]),
to: t.union([dateType, t.string]),
});
export {
ALL_VALUE,

View file

@ -4,11 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const SLOS_BASE_PATH = '/app/slos';
export const SLO_PREFIX = '/slos';
export const SLOS_PATH = '/' as const;
export const SLOS_WELCOME_PATH = '/welcome' as const;
export const SLO_DETAIL_PATH = '/:sloId' as const;
export const SLO_DETAIL_PATH = '/:sloId/:tabId?' as const;
export const SLO_CREATE_PATH = '/create' as const;
export const SLO_EDIT_PATH = '/edit/:sloId' as const;
export const SLOS_OUTDATED_DEFINITIONS_PATH = '/outdated-definitions' as const;
@ -25,10 +26,13 @@ export const paths = {
sloEdit: (sloId: string) => `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}`,
sloEditWithEncodedForm: (sloId: string, encodedParams: string) =>
`${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`,
sloDetails: (sloId: string, instanceId?: string, remoteName?: string) => {
sloDetails: (sloId: string, instanceId?: string, remoteName?: string, tabId?: string) => {
const qs = new URLSearchParams();
if (!!instanceId) qs.append('instanceId', instanceId);
if (!!instanceId && instanceId !== '*') qs.append('instanceId', instanceId);
if (!!remoteName) qs.append('remoteName', remoteName);
if (tabId) {
return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}/${tabId}?${qs.toString()}`;
}
return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?${qs.toString()}`;
},
};

View file

@ -0,0 +1,55 @@
/*
* 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 { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { SloTabId } from '../../../pages/slo_details/components/slo_details';
import { BurnRateOption } from './burn_rates';
interface Props {
burnRateOption: BurnRateOption;
setBurnRateOption: (option: BurnRateOption) => void;
burnRateOptions: BurnRateOption[];
selectedTabId: SloTabId;
}
export function BurnRateHeader({
burnRateOption,
burnRateOptions,
setBurnRateOption,
selectedTabId,
}: Props) {
const onBurnRateOptionChange = (optionId: string) => {
const selected = burnRateOptions.find((opt) => opt.id === optionId) ?? burnRateOptions[0];
setBurnRateOption(selected);
};
return (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.slo.burnRate.title', {
defaultMessage: 'Burn rate',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{selectedTabId !== 'history' && (
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend={i18n.translate('xpack.slo.burnRate.timeRangeBtnLegend', {
defaultMessage: 'Select the time range',
})}
options={burnRateOptions.map((opt) => ({ id: opt.id, label: opt.label }))}
idSelected={burnRateOption.id}
onChange={onBurnRateOptionChange}
buttonSize="compressed"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}

View file

@ -5,11 +5,14 @@
* 2.0.
*/
import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
import { TimeBounds } from '../../../pages/slo_details/types';
import { TimeRange } from '../error_rate_chart/use_lens_definition';
import { SloTabId } from '../../../pages/slo_details/components/slo_details';
import { BurnRateHeader } from './burn_rate_header';
import { useFetchSloBurnRates } from '../../../hooks/use_fetch_slo_burn_rates';
import { ErrorRateChart } from '../error_rate_chart';
import { BurnRate } from './burn_rate';
@ -18,6 +21,9 @@ interface Props {
slo: SLOWithSummaryResponse;
isAutoRefreshing?: boolean;
burnRateOptions: BurnRateOption[];
selectedTabId: SloTabId;
range?: TimeRange;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export interface BurnRateOption {
@ -32,7 +38,14 @@ function getWindowsFromOptions(opts: BurnRateOption[]): Array<{ name: string; du
return opts.map((opt) => ({ name: opt.windowName, duration: `${opt.duration}h` }));
}
export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) {
export function BurnRates({
slo,
isAutoRefreshing,
burnRateOptions,
selectedTabId,
range,
onBrushed,
}: Props) {
const [burnRateOption, setBurnRateOption] = useState(burnRateOptions[0]);
const { isLoading, data } = useFetchSloBurnRates({
slo,
@ -46,12 +59,7 @@ export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) {
}
}, [burnRateOptions]);
const onBurnRateOptionChange = (optionId: string) => {
const selected = burnRateOptions.find((opt) => opt.id === optionId) ?? burnRateOptions[0];
setBurnRateOption(selected);
};
const dataTimeRange = {
const dataTimeRange = range ?? {
from: moment().subtract(burnRateOption.duration, 'hour').toDate(),
to: new Date(),
};
@ -64,34 +72,26 @@ export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) {
return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="burnRatePanel">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.slo.burnRate.title', {
defaultMessage: 'Burn rate',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend={i18n.translate('xpack.slo.burnRate.timeRangeBtnLegend', {
defaultMessage: 'Select the time range',
})}
options={burnRateOptions.map((opt) => ({ id: opt.id, label: opt.label }))}
idSelected={burnRateOption.id}
onChange={onBurnRateOptionChange}
buttonSize="compressed"
/>
</EuiFlexItem>
</EuiFlexGroup>
<BurnRateHeader
burnRateOption={burnRateOption}
burnRateOptions={burnRateOptions}
setBurnRateOption={setBurnRateOption}
selectedTabId={selectedTabId}
/>
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem grow={1}>
<BurnRate threshold={threshold} burnRate={burnRate} slo={slo} isLoading={isLoading} />
</EuiFlexItem>
{selectedTabId !== 'history' && (
<EuiFlexItem grow={1}>
<BurnRate threshold={threshold} burnRate={burnRate} slo={slo} isLoading={isLoading} />
</EuiFlexItem>
)}
<EuiFlexItem grow={3}>
<ErrorRateChart slo={slo} dataTimeRange={dataTimeRange} threshold={threshold} />
<ErrorRateChart
slo={slo}
dataTimeRange={dataTimeRange}
threshold={threshold}
onBrushed={onBrushed}
selectedTabId={selectedTabId}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>

View file

@ -9,6 +9,8 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { SloTabId } from '../../../pages/slo_details/components/slo_details';
import { TimeBounds } from '../../../pages/slo_details/types';
import { useKibana } from '../../../utils/kibana_react';
import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_seconds_from_slo';
import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition';
@ -20,6 +22,8 @@ interface Props {
alertTimeRange?: TimeRange;
showErrorRateAsLine?: boolean;
annotations?: AlertAnnotation[];
selectedTabId?: SloTabId;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function ErrorRateChart({
@ -29,17 +33,20 @@ export function ErrorRateChart({
alertTimeRange,
showErrorRateAsLine,
annotations,
onBrushed,
selectedTabId,
}: Props) {
const {
lens: { EmbeddableComponent },
} = useKibana().services;
const lensDef = useLensDefinition(
const lensDef = useLensDefinition({
slo,
threshold,
alertTimeRange,
annotations,
showErrorRateAsLine
);
showErrorRateAsLine,
selectedTabId,
});
const delayInSeconds = getDelayInSecondsFromSLO(slo);
const from = moment(dataTimeRange.from).subtract(delayInSeconds, 'seconds').toISOString();
@ -55,6 +62,14 @@ export function ErrorRateChart({
}}
attributes={lensDef}
viewMode={ViewMode.VIEW}
onBrushEnd={({ range }) => {
onBrushed?.({
from: range[0],
to: range[1],
fromUtc: moment(range[0]).format(),
toUtc: moment(range[1]).format(),
});
}}
noPadding
/>
);

View file

@ -12,6 +12,7 @@ import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { SloTabId } from '../../../pages/slo_details/components/slo_details';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../common/constants';
export interface TimeRange {
@ -24,13 +25,21 @@ export interface AlertAnnotation {
total: number;
}
export function useLensDefinition(
slo: SLOWithSummaryResponse,
threshold: number,
alertTimeRange?: TimeRange,
annotations?: AlertAnnotation[],
showErrorRateAsLine?: boolean
): TypedLensByValueInput['attributes'] {
export function useLensDefinition({
slo,
threshold,
alertTimeRange,
annotations,
showErrorRateAsLine,
selectedTabId,
}: {
slo: SLOWithSummaryResponse;
threshold: number;
alertTimeRange?: TimeRange;
annotations?: AlertAnnotation[];
showErrorRateAsLine?: boolean;
selectedTabId?: SloTabId;
}): TypedLensByValueInput['attributes'] {
const { euiTheme } = useEuiTheme();
const interval = 'auto';
@ -87,20 +96,24 @@ export function useLensDefinition(
},
],
},
{
layerId: '34298f84-681e-4fa3-8107-d6facb32ed92',
layerType: 'referenceLine',
accessors: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6'],
yConfig: [
{
forAccessor: '0a42b72b-cd5a-4d59-81ec-847d97c268e6',
axisMode: 'left',
textVisibility: true,
color: euiTheme.colors.danger,
iconPosition: 'right',
},
],
},
...(selectedTabId !== 'history'
? [
{
layerId: '34298f84-681e-4fa3-8107-d6facb32ed92',
layerType: 'referenceLine',
accessors: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6'],
yConfig: [
{
forAccessor: '0a42b72b-cd5a-4d59-81ec-847d97c268e6',
axisMode: 'left',
textVisibility: true,
color: euiTheme.colors.danger,
iconPosition: 'right',
},
],
},
]
: []),
...(!!alertTimeRange
? [
{

View file

@ -79,9 +79,9 @@ export function SloOverviewDetails({
{tabs.map((tab, index) => (
<EuiTab
key={index}
onClick={tab.onClick}
onClick={'onClick' in tab ? tab.onClick : undefined}
isSelected={tab.id === selectedTabId}
append={tab.append}
append={'append' in tab ? tab.append : null}
>
{tab.label}
</EuiTab>

View file

@ -23,11 +23,16 @@ export interface UseFetchHistoricalSummaryResponse {
export interface Params {
sloList: SLOWithSummaryResponse[];
shouldRefetch?: boolean;
range?: {
from: string;
to: string;
};
}
export function useFetchHistoricalSummary({
sloList = [],
shouldRefetch,
range,
}: Params): UseFetchHistoricalSummaryResponse {
const { http } = useKibana().services;
@ -40,6 +45,7 @@ export function useFetchHistoricalSummary({
revision: slo.revision,
objective: slo.objective,
budgetingMethod: slo.budgetingMethod,
range,
}));
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({

View file

@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { TimeBounds } from '../types';
import { SloTabId } from './slo_details';
import { useKibana } from '../../../utils/kibana_react';
import { toDuration, toMinutes } from '../../../utils/slo/duration';
import { ChartData } from '../../../typings/slo';
@ -33,9 +35,11 @@ export interface Props {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
selectedTabId?: SloTabId;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function ErrorBudgetChart({ data, isLoading, slo }: Props) {
export function ErrorBudgetChart({ data, isLoading, slo, selectedTabId, onBrushed }: Props) {
const { uiSettings } = useKibana().services;
const percentFormat = uiSettings.get('format:percent:defaultPattern');
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
@ -53,23 +57,12 @@ export function ErrorBudgetChart({ data, isLoading, slo }: Props) {
}
return (
<>
<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 ? (
{selectedTabId !== 'history' && (
<EuiFlexGroup direction="row" gutterSize="l" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={errorBudgetTimeRemainingFormatted}
title={numeral(slo.summary.errorBudget.remaining).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.remaining', {
defaultMessage: 'Remaining',
@ -77,8 +70,24 @@ export function ErrorBudgetChart({ data, isLoading, slo }: Props) {
reverse
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
{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>
)}
<EuiFlexItem>
<WideChart
@ -89,6 +98,7 @@ export function ErrorBudgetChart({ data, isLoading, slo }: Props) {
state={isSloFailed ? 'error' : 'success'}
data={data}
isLoading={isLoading}
onBrushed={onBrushed}
/>
</EuiFlexItem>
</>

View file

@ -14,6 +14,8 @@ import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import React, { useState, useCallback } from 'react';
import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public';
import { TimeBounds } from '../types';
import { SloTabId } from './slo_details';
import { useKibana } from '../../../utils/kibana_react';
import { ChartData } from '../../../typings/slo';
import { ErrorBudgetChart } from './error_budget_chart';
@ -24,9 +26,11 @@ export interface Props {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
selectedTabId: SloTabId;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
export function ErrorBudgetChartPanel({ data, isLoading, slo, selectedTabId, onBrushed }: Props) {
const [isMouseOver, setIsMouseOver] = useState(false);
const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false);
@ -81,9 +85,16 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
showTitle={true}
isMouseOver={isMouseOver}
setDashboardAttachmentReady={setDashboardAttachmentReady}
selectedTabId={selectedTabId}
/>
<ErrorBudgetChart slo={slo} data={data} isLoading={isLoading} />
<ErrorBudgetChart
slo={slo}
data={data}
isLoading={isLoading}
selectedTabId={selectedTabId}
onBrushed={onBrushed}
/>
</EuiFlexGroup>
</EuiPanel>
{isDashboardAttachmentReady ? (

View file

@ -9,6 +9,7 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { SloTabId } from './slo_details';
import { useKibana } from '../../../utils/kibana_react';
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
@ -19,6 +20,7 @@ interface Props {
showTitle?: boolean;
isMouseOver?: boolean;
setDashboardAttachmentReady?: (value: boolean) => void;
selectedTabId?: SloTabId;
}
export function ErrorBudgetHeader({
@ -26,6 +28,7 @@ export function ErrorBudgetHeader({
showTitle = true,
isMouseOver,
setDashboardAttachmentReady,
selectedTabId,
}: Props) {
const { executionContext } = useKibana().services;
const executionContextName = executionContext.get().name;
@ -57,16 +60,18 @@ export function ErrorBudgetHeader({
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
{selectedTabId !== 'history' && (
<EuiFlexItem>
<EuiText color="subdued" size="s">
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}

View file

@ -36,6 +36,9 @@ import { max, min } from 'lodash';
import moment from 'moment';
import React, { useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { TimeBounds } from '../types';
import { getBrushData } from '../../../utils/slo/duration';
import { SloTabId } from './slo_details';
import { useGetPreviewData } from '../../../hooks/use_get_preview_data';
import { useKibana } from '../../../utils/kibana_react';
import { COMPARATOR_MAPPING } from '../../slo_edit/constants';
@ -48,9 +51,11 @@ export interface Props {
start: number;
end: number;
};
selectedTabId: SloTabId;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function EventsChartPanel({ slo, range }: Props) {
export function EventsChartPanel({ slo, range, selectedTabId, onBrushed }: Props) {
const { charts, uiSettings, discover } = useKibana().services;
const { euiTheme } = useEuiTheme();
const baseTheme = charts.theme.useChartsBaseTheme();
@ -157,13 +162,15 @@ export function EventsChartPanel({ slo, range }: Props) {
<EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={1}> {title}</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.slo.sloDetails.eventsChartPanel.duration', {
defaultMessage: 'Last 24h',
})}
</EuiText>
</EuiFlexItem>
{selectedTabId !== 'history' && (
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.slo.sloDetails.eventsChartPanel.duration', {
defaultMessage: 'Last 24h',
})}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
{showViewEventsLink && (
<EuiFlexItem grow={0}>
@ -193,6 +200,7 @@ export function EventsChartPanel({ slo, range }: Props) {
data={data || []}
annotation={annotation}
slo={slo}
onBrushed={onBrushed}
/>
) : (
<>
@ -225,6 +233,9 @@ export function EventsChartPanel({ slo, range }: Props) {
pointerUpdateDebounce={0}
pointerUpdateTrigger={'x'}
locale={i18n.getLocale()}
onBrushEnd={(brushArea) => {
onBrushed?.(getBrushData(brushArea));
}}
/>
{annotation}

View file

@ -0,0 +1,75 @@
/*
* 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 { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { TimeBounds } from '../types';
import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary';
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
import { SloTabId } from './slo_details';
import { SliChartPanel } from './sli_chart_panel';
import { ErrorBudgetChartPanel } from './error_budget_chart_panel';
export interface Props {
slo: SLOWithSummaryResponse;
isAutoRefreshing: boolean;
selectedTabId: SloTabId;
range?: {
from: string;
to: string;
};
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function HistoricalDataCharts({
slo,
range,
isAutoRefreshing,
selectedTabId,
onBrushed,
}: Props) {
const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } =
useFetchHistoricalSummary({
sloList: [slo],
shouldRefetch: isAutoRefreshing,
range,
});
const sloHistoricalSummary = historicalSummaries.find(
(historicalSummary) =>
historicalSummary.sloId === slo.id &&
historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE)
);
const errorBudgetBurnDownData = formatHistoricalData(
sloHistoricalSummary?.data,
'error_budget_remaining'
);
const historicalSliData = formatHistoricalData(sloHistoricalSummary?.data, 'sli_value');
return (
<>
<EuiFlexItem>
<SliChartPanel
data={historicalSliData}
isLoading={historicalSummaryLoading}
slo={slo}
selectedTabId={selectedTabId}
onBrushed={onBrushed}
/>
</EuiFlexItem>
<EuiFlexItem>
<ErrorBudgetChartPanel
data={errorBudgetBurnDownData}
isLoading={historicalSummaryLoading}
slo={slo}
selectedTabId={selectedTabId}
onBrushed={onBrushed}
/>
</EuiFlexItem>
</>
);
}

View file

@ -0,0 +1,128 @@
/*
* 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 React, { useCallback, useMemo, useState } from 'react';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
OnTimeChangeProps,
OnRefreshProps,
EuiSpacer,
} from '@elastic/eui';
import DateMath from '@kbn/datemath';
import { useKibana } from '../../../../utils/kibana_react';
import { HistoricalDataCharts } from '../historical_data_charts';
import { useBurnRateOptions } from '../../hooks/use_burn_rate_options';
import { SloTabId } from '../slo_details';
import { BurnRates } from '../../../../components/slo/burn_rate/burn_rates';
import { EventsChartPanel } from '../events_chart_panel';
export interface Props {
slo: SLOWithSummaryResponse;
isAutoRefreshing: boolean;
selectedTabId: SloTabId;
}
export function SLODetailsHistory({ slo, isAutoRefreshing, selectedTabId }: Props) {
const { uiSettings } = useKibana().services;
const { burnRateOptions } = useBurnRateOptions(slo);
const [start, setStart] = useState('now-30d');
const [end, setEnd] = useState('now');
const onTimeChange = (val: OnTimeChangeProps) => {
setStart(val.start);
setEnd(val.end);
};
const onRefresh = (val: OnRefreshProps) => {};
const absRange = useMemo(() => {
return {
from: new Date(DateMath.parse(start)!.valueOf()),
to: new Date(DateMath.parse(end, { roundUp: true })!.valueOf()),
absoluteFrom: DateMath.parse(start)!.valueOf(),
absoluteTo: DateMath.parse(end, { roundUp: true })!.valueOf(),
};
}, [start, end]);
const onBrushed = useCallback(({ fromUtc, toUtc }) => {
setStart(fromUtc);
setEnd(toUtc);
}, []);
return (
<>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem
grow
css={{
maxWidth: 500,
}}
>
<EuiSuperDatePicker
isLoading={false}
start={start}
end={end}
onTimeChange={onTimeChange}
onRefresh={onRefresh}
width="full"
commonlyUsedRanges={uiSettings
.get('timepicker:quickRanges')
.map(({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem>
<BurnRates
slo={slo}
isAutoRefreshing={isAutoRefreshing}
burnRateOptions={burnRateOptions}
selectedTabId={selectedTabId}
range={{
from: absRange.from,
to: absRange.to,
}}
onBrushed={onBrushed}
/>
</EuiFlexItem>
<HistoricalDataCharts
slo={slo}
selectedTabId={selectedTabId}
isAutoRefreshing={isAutoRefreshing}
range={{
from: start,
to: end,
}}
onBrushed={onBrushed}
/>
<EuiFlexItem>
<EventsChartPanel
slo={slo}
range={{
start: absRange.absoluteFrom,
end: absRange.absoluteTo,
}}
selectedTabId={selectedTabId}
onBrushed={onBrushed}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -10,6 +10,8 @@ import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
import { TimeBounds } from '../types';
import { SloTabId } from './slo_details';
import { useKibana } from '../../../utils/kibana_react';
import { ChartData } from '../../../typings/slo';
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
@ -19,9 +21,11 @@ export interface Props {
data: ChartData[];
isLoading: boolean;
slo: SLOWithSummaryResponse;
selectedTabId: SloTabId;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function SliChartPanel({ data, isLoading, slo }: Props) {
export function SliChartPanel({ data, isLoading, slo, selectedTabId, onBrushed }: Props) {
const { uiSettings } = useKibana().services;
const percentFormat = uiSettings.get('format:percent:defaultPattern');
@ -41,41 +45,45 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
{selectedTabId !== 'history' && (
<EuiFlexItem>
<EuiText color="subdued" size="s">
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
? i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.duration', {
defaultMessage: 'Last {duration}',
values: { duration: toDurationLabel(slo.timeWindow.duration) },
})
: toDurationAdverbLabel(slo.timeWindow.duration)}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="l" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={hasNoData ? '-' : numeral(slo.summary.sliValue).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.current', {
defaultMessage: 'Observed value',
})}
reverse
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
title={numeral(slo.objective.target).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.objective', {
defaultMessage: 'Objective',
})}
reverse
/>
</EuiFlexItem>
</EuiFlexGroup>
{selectedTabId !== 'history' && (
<EuiFlexGroup direction="row" gutterSize="l" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiStat
titleColor={isSloFailed ? 'danger' : 'success'}
title={hasNoData ? '-' : numeral(slo.summary.sliValue).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.current', {
defaultMessage: 'Observed value',
})}
reverse
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
title={numeral(slo.objective.target).format(percentFormat)}
titleSize="s"
description={i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.objective', {
defaultMessage: 'Objective',
})}
reverse
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexItem>
<WideChart
@ -86,6 +94,7 @@ export function SliChartPanel({ data, isLoading, slo }: Props) {
state={isSloFailed ? 'error' : 'success'}
data={data}
isLoading={isLoading}
onBrushed={onBrushed}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -5,71 +5,26 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import React, { useEffect, useState } from 'react';
import { BurnRateOption, BurnRates } from '../../../components/slo/burn_rate/burn_rates';
import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary';
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
import { ErrorBudgetChartPanel } from './error_budget_chart_panel';
import { HistoricalDataCharts } from './historical_data_charts';
import { useBurnRateOptions } from '../hooks/use_burn_rate_options';
import { SLODetailsHistory } from './history/slo_details_history';
import { BurnRates } from '../../../components/slo/burn_rate/burn_rates';
import { EventsChartPanel } from './events_chart_panel';
import { Overview } from './overview/overview';
import { SliChartPanel } from './sli_chart_panel';
import { SloDetailsAlerts } from './slo_detail_alerts';
import { SloHealthCallout } from './slo_health_callout';
import { SloRemoteCallout } from './slo_remote_callout';
export const TAB_ID_URL_PARAM = 'tabId';
export const OVERVIEW_TAB_ID = 'overview';
export const HISTORY_TAB_ID = 'history';
export const ALERTS_TAB_ID = 'alerts';
const DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
const DEFAULT_BURN_RATE_OPTIONS: BurnRateOption[] = [
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 1 },
}),
windowName: 'CRITICAL',
threshold: 14.4,
duration: 1,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 6 },
}),
windowName: 'HIGH',
threshold: 6,
duration: 6,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 24 },
}),
windowName: 'MEDIUM',
threshold: 3,
duration: 24,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 72 },
}),
windowName: 'LOW',
threshold: 1,
duration: 72,
},
];
export type SloTabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID;
export type SloTabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID | typeof HISTORY_TAB_ID;
export interface Props {
slo: SLOWithSummaryResponse;
@ -77,30 +32,7 @@ export interface Props {
selectedTabId: SloTabId;
}
export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) {
const { data: rules } = useFetchRulesForSlo({ sloIds: [slo.id] });
const burnRateOptions =
rules?.[slo.id]?.[0]?.params?.windows?.map((window) => ({
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: window.longWindow.value },
}),
windowName: window.actionGroup,
threshold: window.burnRateThreshold,
duration: window.longWindow.value,
})) ?? DEFAULT_BURN_RATE_OPTIONS;
const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } =
useFetchHistoricalSummary({
sloList: [slo],
shouldRefetch: isAutoRefreshing,
});
const sloHistoricalSummary = historicalSummaries.find(
(historicalSummary) =>
historicalSummary.sloId === slo.id &&
historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE)
);
const { burnRateOptions } = useBurnRateOptions(slo);
const [range, setRange] = useState({
start: new Date().getTime() - DAY_IN_MILLISECONDS,
@ -118,12 +50,6 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) {
return () => clearInterval(intervalId);
}, [isAutoRefreshing]);
const errorBudgetBurnDownData = formatHistoricalData(
sloHistoricalSummary?.data,
'error_budget_remaining'
);
const historicalSliData = formatHistoricalData(sloHistoricalSummary?.data, 'sli_value');
return selectedTabId === OVERVIEW_TAB_ID ? (
<EuiFlexGroup direction="column" gutterSize="xl">
<SloRemoteCallout slo={slo} />
@ -137,24 +63,26 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) {
slo={slo}
isAutoRefreshing={isAutoRefreshing}
burnRateOptions={burnRateOptions}
selectedTabId={selectedTabId}
/>
</EuiFlexItem>
<HistoricalDataCharts
slo={slo}
selectedTabId={selectedTabId}
isAutoRefreshing={isAutoRefreshing}
/>
<EuiFlexItem>
<SliChartPanel data={historicalSliData} isLoading={historicalSummaryLoading} slo={slo} />
</EuiFlexItem>
<EuiFlexItem>
<ErrorBudgetChartPanel
data={errorBudgetBurnDownData}
isLoading={historicalSummaryLoading}
slo={slo}
/>
</EuiFlexItem>
<EuiFlexItem>
<EventsChartPanel slo={slo} range={range} />
<EventsChartPanel slo={slo} range={range} selectedTabId={selectedTabId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
) : (
) : selectedTabId === ALERTS_TAB_ID ? (
<SloDetailsAlerts slo={slo} />
) : (
<SLODetailsHistory
slo={slo}
isAutoRefreshing={isAutoRefreshing}
selectedTabId={selectedTabId}
/>
);
}

View file

@ -24,6 +24,8 @@ import moment from 'moment';
import React, { useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { getBrushData } from '../../../utils/slo/duration';
import { TimeBounds } from '../types';
import { useKibana } from '../../../utils/kibana_react';
import { ChartData } from '../../../typings';
@ -36,9 +38,10 @@ export interface Props {
chart: ChartType;
state: State;
isLoading: boolean;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function WideChart({ chart, data, id, isLoading, state }: Props) {
export function WideChart({ chart, data, id, isLoading, state, onBrushed }: Props) {
const { charts, uiSettings } = useKibana().services;
const baseTheme = charts.theme.useChartsBaseTheme();
const { euiTheme } = useEuiTheme();
@ -63,7 +66,16 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) {
<Settings
baseTheme={baseTheme}
showLegend={false}
noResults={<EuiIcon type="visualizeApp" size="l" color="subdued" title="no results" />}
noResults={
<EuiIcon
type="visualizeApp"
size="l"
color="subdued"
title={i18n.translate('xpack.slo.wideChart.euiIcon.noResultsLabel', {
defaultMessage: 'no results',
})}
/>
}
onPointerUpdate={handleCursorUpdate}
externalPointerEvents={{
tooltip: { visible: true },
@ -71,6 +83,9 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) {
pointerUpdateDebounce={0}
pointerUpdateTrigger={'x'}
locale={i18n.getLocale()}
onBrushEnd={(brushArea) => {
onBrushed?.(getBrushData(brushArea));
}}
/>
<Axis
id="bottom"

View file

@ -0,0 +1,72 @@
/*
* 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 { htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { BurnRateOption } from '../../../components/slo/burn_rate/burn_rates';
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
export const DEFAULT_BURN_RATE_OPTIONS: BurnRateOption[] = [
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 1 },
}),
windowName: 'CRITICAL',
threshold: 14.4,
duration: 1,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 6 },
}),
windowName: 'HIGH',
threshold: 6,
duration: 6,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 24 },
}),
windowName: 'MEDIUM',
threshold: 3,
duration: 24,
},
{
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: 72 },
}),
windowName: 'LOW',
threshold: 1,
duration: 72,
},
];
export const useBurnRateOptions = (slo: SLOWithSummaryResponse) => {
const { data: rules } = useFetchRulesForSlo({ sloIds: [slo.id] });
const burnRateOptions =
rules?.[slo.id]?.[0]?.params?.windows?.map((window) => ({
id: htmlIdGenerator()(),
label: i18n.translate('xpack.slo.burnRates.fromRange.label', {
defaultMessage: '{duration}h',
values: { duration: window.longWindow.value },
}),
windowName: window.actionGroup,
threshold: window.burnRateThreshold,
duration: window.longWindow.value,
})) ?? DEFAULT_BURN_RATE_OPTIONS;
return { burnRateOptions };
};

View file

@ -0,0 +1,38 @@
/*
* 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 { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { SloDetailsPathParams } from '../types';
import {
ALERTS_TAB_ID,
HISTORY_TAB_ID,
OVERVIEW_TAB_ID,
SloTabId,
} from '../components/slo_details';
export const useSelectedTab = () => {
const { tabId } = useParams<SloDetailsPathParams>();
const [selectedTabId, setSelectedTabId] = useState(() => {
return tabId && [OVERVIEW_TAB_ID, ALERTS_TAB_ID, HISTORY_TAB_ID].includes(tabId)
? (tabId as SloTabId)
: OVERVIEW_TAB_ID;
});
useEffect(() => {
// update the url when the selected tab changes
if (tabId !== selectedTabId) {
setSelectedTabId(tabId as SloTabId);
}
}, [selectedTabId, tabId]);
return {
selectedTabId: selectedTabId || OVERVIEW_TAB_ID,
setSelectedTabId,
};
};

View file

@ -9,8 +9,15 @@ import { i18n } from '@kbn/i18n';
import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { paths } from '../../../../common/locators/paths';
import { useKibana } from '../../../utils/kibana_react';
import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts';
import { ALERTS_TAB_ID, OVERVIEW_TAB_ID, SloTabId } from '../components/slo_details';
import {
ALERTS_TAB_ID,
HISTORY_TAB_ID,
OVERVIEW_TAB_ID,
SloTabId,
} from '../components/slo_details';
export const useSloDetailsTabs = ({
slo,
@ -21,13 +28,15 @@ export const useSloDetailsTabs = ({
slo?: SLOWithSummaryResponse | null;
isAutoRefreshing: boolean;
selectedTabId: SloTabId;
setSelectedTabId: (val: SloTabId) => void;
setSelectedTabId?: (val: SloTabId) => void;
}) => {
const { data: activeAlerts } = useFetchActiveAlerts({
sloIdsAndInstanceIds: slo ? [[slo.id, slo.instanceId ?? ALL_VALUE]] : [],
shouldRefetch: isAutoRefreshing,
});
const { basePath } = useKibana().services.http;
const isRemote = !!slo?.remote;
const tabs = [
@ -38,8 +47,47 @@ export const useSloDetailsTabs = ({
}),
'data-test-subj': 'overviewTab',
isSelected: selectedTabId === OVERVIEW_TAB_ID,
onClick: () => setSelectedTabId(OVERVIEW_TAB_ID),
...(setSelectedTabId
? {
onClick: () => setSelectedTabId(OVERVIEW_TAB_ID),
}
: {
href: slo
? `${basePath.get()}${paths.sloDetails(
slo.id,
slo.instanceId,
slo.remote?.remoteName,
OVERVIEW_TAB_ID
)}`
: undefined,
}),
},
...(slo?.timeWindow.type === 'rolling'
? [
{
id: HISTORY_TAB_ID,
label: i18n.translate('xpack.slo.sloDetails.tab.historyLabel', {
defaultMessage: 'History',
}),
'data-test-subj': 'historyTab',
isSelected: selectedTabId === HISTORY_TAB_ID,
...(setSelectedTabId
? {
onClick: () => setSelectedTabId(HISTORY_TAB_ID),
}
: {
href: slo
? `${basePath.get()}${paths.sloDetails(
slo.id,
slo.instanceId,
slo.remote?.remoteName,
HISTORY_TAB_ID
)}`
: undefined,
}),
},
]
: []),
{
id: ALERTS_TAB_ID,
label: isRemote ? (
@ -63,7 +111,20 @@ export const useSloDetailsTabs = ({
{(activeAlerts && activeAlerts.get(slo)) ?? 0}
</EuiNotificationBadge>
) : null,
onClick: () => setSelectedTabId(ALERTS_TAB_ID),
...(setSelectedTabId
? {
onClick: () => setSelectedTabId(ALERTS_TAB_ID),
}
: {
href: slo
? `${basePath.get()}${paths.sloDetails(
slo.id,
slo.instanceId,
slo.remote?.remoteName,
ALERTS_TAB_ID
)}`
: undefined,
}),
},
];

View file

@ -79,6 +79,7 @@ const mockKibana = () => {
http: {
basePath: {
prepend: (url: string) => url,
get: () => 'http://localhost:5601',
},
},
dataViews: {

View file

@ -6,7 +6,7 @@
*/
import React, { useEffect, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useIsMutating } from '@tanstack/react-query';
import { EuiLoadingSpinner, EuiSkeletonText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -16,6 +16,7 @@ import type { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import dedent from 'dedent';
import { useSelectedTab } from './hooks/use_selected_tab';
import { HeaderMenu } from '../../components/header_menu/header_menu';
import { useSloDetailsTabs } from './hooks/use_slo_details_tabs';
import { useKibana } from '../../utils/kibana_react';
@ -23,13 +24,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context';
import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details';
import { useLicense } from '../../hooks/use_license';
import PageNotFound from '../404';
import {
ALERTS_TAB_ID,
OVERVIEW_TAB_ID,
SloDetails,
TAB_ID_URL_PARAM,
SloTabId,
} from './components/slo_details';
import { SloDetails } from './components/slo_details';
import { HeaderTitle } from './components/header_title';
import { HeaderControl } from './components/header_control';
import { paths } from '../../../common/locators/paths';
@ -45,7 +40,6 @@ export function SloDetailsPage() {
observabilityAIAssistant,
} = useKibana().services;
const { ObservabilityPageTemplate } = usePluginContext();
const { search } = useLocation();
const { hasAtLeast } = useLicense();
const hasRightLicense = hasAtLeast('platinum');
@ -61,19 +55,12 @@ export function SloDetailsPage() {
});
const isDeleting = Boolean(useIsMutating(['deleteSlo']));
const [selectedTabId, setSelectedTabId] = useState(() => {
const searchParams = new URLSearchParams(search);
const urlTabId = searchParams.get(TAB_ID_URL_PARAM);
return urlTabId && [OVERVIEW_TAB_ID, ALERTS_TAB_ID].includes(urlTabId)
? (urlTabId as SloTabId)
: OVERVIEW_TAB_ID;
});
const { selectedTabId } = useSelectedTab();
const { tabs } = useSloDetailsTabs({
slo,
isAutoRefreshing,
selectedTabId,
setSelectedTabId,
});
useBreadcrumbs(getBreadcrumbs(basePath, slo));

View file

@ -7,4 +7,12 @@
export interface SloDetailsPathParams {
sloId: string;
tabId?: string;
}
export interface TimeBounds {
from: number;
to: number;
fromUtc: string;
toUtc: string;
}

View file

@ -23,6 +23,8 @@ import { i18n } from '@kbn/i18n';
import { GetPreviewDataResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React, { useRef } from 'react';
import { TimeBounds } from '../../../slo_details/types';
import { getBrushData } from '../../../../utils/slo/duration';
import { useKibana } from '../../../../utils/kibana_react';
import { openInDiscover } from '../../../../utils/slo/get_discover_link';
@ -32,6 +34,7 @@ export interface Props {
annotation?: React.ReactNode;
isLoading?: boolean;
bottomTitle?: string;
onBrushed?: (timeBounds: TimeBounds) => void;
}
export function GoodBadEventsChart({
@ -39,6 +42,7 @@ export function GoodBadEventsChart({
bottomTitle,
data,
slo,
onBrushed,
isLoading = false,
}: Props) {
const { charts, uiSettings, discover } = useKibana().services;
@ -97,7 +101,16 @@ export function GoodBadEventsChart({
showLegend={true}
showLegendExtra={false}
legendPosition={Position.Left}
noResults={<EuiIcon type="visualizeApp" size="l" color="subdued" title="no results" />}
noResults={
<EuiIcon
type="visualizeApp"
size="l"
color="subdued"
title={i18n.translate('xpack.slo.goodBadEventsChart.euiIcon.noResultsLabel', {
defaultMessage: 'no results',
})}
/>
}
onPointerUpdate={handleCursorUpdate}
externalPointerEvents={{
tooltip: { visible: true },
@ -106,6 +119,9 @@ export function GoodBadEventsChart({
pointerUpdateTrigger={'x'}
locale={i18n.getLocale()}
onElementClick={barClickHandler as ElementClickListener}
onBrushEnd={(brushArea) => {
onBrushed?.(getBrushData(brushArea));
}}
/>
{annotation}
<Axis

View file

@ -7,6 +7,7 @@
import moment from 'moment';
import { assertNever } from '@kbn/std';
import { BrushEvent } from '@elastic/charts';
import { Duration, DurationUnit } from '../../typings';
export function toDuration(duration: string): Duration {
@ -42,3 +43,10 @@ export function toCalendarAlignedMomentUnitOfTime(unit: string): moment.unitOfTi
return 'months';
}
}
export function getBrushData(e: BrushEvent) {
const [from, to] = [Number(e.x?.[0]), Number(e.x?.[1])];
const [fromUtc, toUtc] = [moment(from).format(), moment(to).format()];
return { from, to, fromUtc, toUtc };
}

View file

@ -19,6 +19,7 @@ import {
occurrencesBudgetingMethodSchema,
timeslicesBudgetingMethodSchema,
} from '@kbn/slo-schema';
import { getEsDateRange } from './historical_summary_client';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants';
import { DateRange, Duration, SLODefinition } from '../domain/models';
import { computeBurnRate, computeSLI } from '../domain/services';
@ -98,7 +99,7 @@ function commonQuery(
{ term: { 'slo.revision': slo.revision } },
{
range: {
'@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() },
'@timestamp': getEsDateRange(dateRange),
},
},
];

View file

@ -64,8 +64,8 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient {
async fetch(params: FetchHistoricalSummaryParams): Promise<HistoricalSummaryResponse> {
const dateRangeBySlo = params.list.reduce<Record<SLOId, DateRange>>(
(acc, { sloId, timeWindow }) => {
acc[sloId] = getDateRange(timeWindow);
(acc, { sloId, timeWindow, range }) => {
acc[sloId] = range ?? getDateRange(timeWindow);
return acc;
},
{}
@ -272,6 +272,13 @@ function handleResultForRollingAndTimeslices(
});
}
export const getEsDateRange = (dateRange: DateRange) => {
return {
gte: typeof dateRange.from === 'string' ? dateRange.from : dateRange.from.toISOString(),
lte: typeof dateRange.to === 'string' ? dateRange.to : dateRange.to.toISOString(),
};
};
function generateSearchQuery({
sloId,
groupBy,
@ -309,10 +316,7 @@ function generateSearchQuery({
{ term: { 'slo.revision': revision } },
{
range: {
'@timestamp': {
gte: dateRange.from.toISOString(),
lte: dateRange.to.toISOString(),
},
'@timestamp': getEsDateRange(dateRange),
},
},
...extraFilterByInstanceId,
@ -325,7 +329,7 @@ function generateSearchQuery({
field: '@timestamp',
fixed_interval: fixedInterval,
extended_bounds: {
min: dateRange.from.toISOString(),
min: typeof dateRange.from === 'string' ? dateRange.from : dateRange.from.toISOString(),
max: 'now/d',
},
},

View file

@ -58,7 +58,7 @@ describe('SummaryClient', () => {
{ term: { 'slo.revision': slo.revision } },
{
range: {
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
'@timestamp': { gte: expect.anything(), lte: expect.anything() },
},
},
],
@ -94,7 +94,7 @@ describe('SummaryClient', () => {
range: {
'@timestamp': {
gte: expect.anything(),
lt: expect.anything(),
lte: expect.anything(),
},
},
},
@ -136,7 +136,7 @@ describe('SummaryClient', () => {
{ term: { 'slo.revision': slo.revision } },
{
range: {
'@timestamp': { gte: expect.anything(), lt: expect.anything() },
'@timestamp': { gte: expect.anything(), lte: expect.anything() },
},
},
],
@ -188,7 +188,7 @@ describe('SummaryClient', () => {
range: {
'@timestamp': {
gte: expect.anything(),
lt: expect.anything(),
lte: expect.anything(),
},
},
},

View file

@ -16,6 +16,7 @@ import {
occurrencesBudgetingMethodSchema,
timeslicesBudgetingMethodSchema,
} from '@kbn/slo-schema';
import { getEsDateRange } from './historical_summary_client';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants';
import { Groupings, Meta, SLODefinition, Summary } from '../domain/models';
import { computeSLI, computeSummaryStatus, toErrorBudget } from '../domain/services';
@ -75,7 +76,7 @@ export class DefaultSummaryClient implements SummaryClient {
{ term: { 'slo.revision': slo.revision } },
{
range: {
'@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() },
'@timestamp': getEsDateRange(dateRange),
},
},
...instanceIdFilter,

View file

@ -97,6 +97,7 @@
"@kbn/data-view-field-editor-plugin",
"@kbn/securitysolution-io-ts-utils",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/datemath",
"@kbn/presentation-containers",
]
}