mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
feat(slo): create burn rate embeddable (#189429)
This commit is contained in:
parent
8481715534
commit
e689da1bc6
15 changed files with 739 additions and 49 deletions
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { EuiIcon, EuiLoadingChart, EuiStat, EuiTextColor, EuiToolTip } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useFetchSloBurnRates } from '../../../hooks/use_fetch_slo_burn_rates';
|
||||
import { toDuration, toMinutes } from '../../../utils/slo/duration';
|
||||
|
||||
interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
duration: string;
|
||||
lastRefreshTime?: number;
|
||||
}
|
||||
|
||||
export function SimpleBurnRate({ slo, duration, lastRefreshTime }: Props) {
|
||||
const [refreshTime, setRefreshTime] = React.useState(lastRefreshTime);
|
||||
const { isLoading, data, refetch } = useFetchSloBurnRates({
|
||||
slo,
|
||||
windows: [{ name: 'burn_rate', duration }],
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (lastRefreshTime !== refreshTime) {
|
||||
setRefreshTime(lastRefreshTime);
|
||||
refetch();
|
||||
}
|
||||
}, [refreshTime, lastRefreshTime, refetch]);
|
||||
|
||||
const durationLabel = i18n.translate('xpack.slo.burnRate.durationLabel', {
|
||||
defaultMessage: 'Last {duration}',
|
||||
values: { duration },
|
||||
});
|
||||
|
||||
if (isLoading || data === undefined) {
|
||||
return (
|
||||
<EuiStat
|
||||
title={<EuiLoadingChart />}
|
||||
textAlign="left"
|
||||
isLoading={isLoading}
|
||||
titleColor={'subdued'}
|
||||
description={
|
||||
<EuiTextColor color="subdued">
|
||||
<span>
|
||||
<EuiIcon type="clock" color={'subdued'} /> {durationLabel}
|
||||
</span>
|
||||
</EuiTextColor>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const burnRate = data.burnRates[0];
|
||||
const color = burnRate.burnRate > 1 ? 'danger' : 'success';
|
||||
const timeToExhaustLabel = i18n.translate('xpack.slo.burnRate.exhaustionTimeLabel', {
|
||||
defaultMessage: 'At this rate, the entire error budget will be exhausted in {hour} hours.',
|
||||
values: {
|
||||
hour: numeral(
|
||||
moment
|
||||
.duration(toMinutes(toDuration(slo.timeWindow.duration)) / burnRate.burnRate, 'minutes')
|
||||
.asHours()
|
||||
).format('0'),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiStat
|
||||
title={i18n.translate('xpack.slo.burnRates.value', {
|
||||
defaultMessage: '{value}x',
|
||||
values: { value: numeral(burnRate.burnRate).format('0.00') },
|
||||
})}
|
||||
textAlign="left"
|
||||
isLoading={isLoading}
|
||||
titleColor={color}
|
||||
description={
|
||||
burnRate.burnRate > 1 ? (
|
||||
<EuiToolTip position="top" content={timeToExhaustLabel}>
|
||||
<EuiTextColor color={color}>
|
||||
<span>
|
||||
<EuiIcon type="clock" color={color} /> {durationLabel}
|
||||
</span>
|
||||
</EuiTextColor>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiTextColor color={color}>
|
||||
<span>
|
||||
<EuiIcon type="clock" color={color} /> {durationLabel}
|
||||
</span>
|
||||
</EuiTextColor>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -29,6 +29,7 @@ export function SloSelector({ initialSlos, onSelected, hasError, singleSelection
|
|||
label: slo.instanceId !== ALL_VALUE ? `${slo.name} (${slo.instanceId})` : slo.name,
|
||||
value: `${slo.id}-${slo.instanceId}`,
|
||||
})) ?? [];
|
||||
|
||||
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
mapSlosToOptions(initialSlos)
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingChart } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { SimpleBurnRate } from '../../../components/slo/simple_burn_rate/burn_rate';
|
||||
import { useFetchSloDetails } from '../../../hooks/use_fetch_slo_details';
|
||||
import { SloOverviewDetails } from '../common/slo_overview_details';
|
||||
import { EmbeddableProps } from './types';
|
||||
|
||||
export function BurnRate({ sloId, sloInstanceId, duration, reloadSubject }: EmbeddableProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState<number | undefined>(undefined);
|
||||
const [selectedSlo, setSelectedSlo] = useState<SLOWithSummaryResponse | null>(null);
|
||||
const [showAllGroups, setShowAllGroups] = useState(false);
|
||||
|
||||
const { isLoading, data: slo } = useFetchSloDetails({
|
||||
sloId,
|
||||
instanceId: sloInstanceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reloadSubject?.subscribe(() => {
|
||||
setLastRefreshTime(Date.now());
|
||||
});
|
||||
|
||||
return () => {
|
||||
reloadSubject?.unsubscribe();
|
||||
};
|
||||
}, [reloadSubject]);
|
||||
|
||||
const isSloNotFound = !isLoading && slo === undefined;
|
||||
|
||||
if (isLoading || !slo) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={container}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingChart />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSloNotFound) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={container}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.slo.sloEmbeddable.overview.sloNotFoundText', {
|
||||
defaultMessage:
|
||||
'The SLO has been deleted. You can safely delete the widget from the dashboard.',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const hasGroupings = Object.keys(slo.groupings).length > 0;
|
||||
const firstGrouping = hasGroupings ? Object.entries(slo.groupings)[0] : undefined;
|
||||
const firstGroupLabel = firstGrouping ? `${firstGrouping[0]}: ${firstGrouping[1]}` : null;
|
||||
const hasMoreThanOneGrouping = Object.keys(slo.groupings).length > 1;
|
||||
|
||||
return (
|
||||
<div data-shared-item="" ref={containerRef} style={{ width: '100%', padding: 10 }}>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
data-test-subj="sloBurnRateLink"
|
||||
className={link}
|
||||
color="text"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
<h2>{slo.name}</h2>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
{hasGroupings && (
|
||||
<EuiFlexGroup direction="row" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge>{firstGroupLabel}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
{hasMoreThanOneGrouping && !showAllGroups ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
onClick={() => setShowAllGroups(true)}
|
||||
onClickAriaLabel={i18n.translate(
|
||||
'xpack.slo.burnRateEmbeddable.moreInstanceAriaLabel',
|
||||
{ defaultMessage: 'Show more' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.burnRateEmbeddable.moreInstanceLabel"
|
||||
defaultMessage="+{groupingsMore} more instance"
|
||||
values={{ groupingsMore: Object.keys(slo.groupings).length - 1 }}
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
|
||||
{hasMoreThanOneGrouping && showAllGroups
|
||||
? Object.entries(slo.groupings)
|
||||
.splice(1)
|
||||
.map(([key, value]) => (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge>
|
||||
{key}: {value}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))
|
||||
: null}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="row" justifyContent="flexEnd">
|
||||
<SimpleBurnRate slo={slo} duration={duration} lastRefreshTime={lastRefreshTime} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<SloOverviewDetails slo={selectedSlo} setSelectedSlo={setSelectedSlo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const container = css`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const link = css`
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
`;
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 React, { useEffect } from 'react';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
initializeTitles,
|
||||
useBatchedPublishingSubjects,
|
||||
fetch$,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SLO_BURN_RATE_EMBEDDABLE_ID } from './constants';
|
||||
import { SloBurnRateEmbeddableState, SloEmbeddableDeps, BurnRateApi } from './types';
|
||||
import { BurnRate } from './burn_rate';
|
||||
|
||||
export const getTitle = () =>
|
||||
i18n.translate('xpack.slo.burnRateEmbeddable.title', {
|
||||
defaultMessage: 'SLO Burn Rate',
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const getBurnRateEmbeddableFactory = (deps: SloEmbeddableDeps) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
SloBurnRateEmbeddableState,
|
||||
SloBurnRateEmbeddableState,
|
||||
BurnRateApi
|
||||
> = {
|
||||
type: SLO_BURN_RATE_EMBEDDABLE_ID,
|
||||
deserializeState: (state) => {
|
||||
return state.rawState as SloBurnRateEmbeddableState;
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
|
||||
const defaultTitle$ = new BehaviorSubject<string | undefined>(getTitle());
|
||||
const sloId$ = new BehaviorSubject(state.sloId);
|
||||
const sloInstanceId$ = new BehaviorSubject(state.sloInstanceId);
|
||||
const duration$ = new BehaviorSubject(state.duration);
|
||||
const reload$ = new Subject<boolean>();
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
...titlesApi,
|
||||
defaultPanelTitle: defaultTitle$,
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {
|
||||
...serializeTitles(),
|
||||
sloId: sloId$.getValue(),
|
||||
sloInstanceId: sloInstanceId$.getValue(),
|
||||
duration: duration$.getValue(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
sloId: [sloId$, (value) => sloId$.next(value)],
|
||||
sloInstanceId: [sloInstanceId$, (value) => sloInstanceId$.next(value)],
|
||||
duration: [duration$, (value) => duration$.next(value)],
|
||||
...titleComparators,
|
||||
}
|
||||
);
|
||||
|
||||
const fetchSubscription = fetch$(api)
|
||||
.pipe()
|
||||
.subscribe((next) => {
|
||||
reload$.next(next.isReload);
|
||||
});
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [sloId, sloInstanceId, duration] = useBatchedPublishingSubjects(
|
||||
sloId$,
|
||||
sloInstanceId$,
|
||||
duration$
|
||||
);
|
||||
|
||||
const I18nContext = deps.i18n.Context;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fetchSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<I18nContext>
|
||||
<Router history={createBrowserHistory()}>
|
||||
<KibanaContextProvider services={deps}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BurnRate
|
||||
sloId={sloId}
|
||||
sloInstanceId={sloInstanceId}
|
||||
duration={duration}
|
||||
reloadSubject={reload$}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>
|
||||
</Router>
|
||||
</I18nContext>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return factory;
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import React, { useState } from 'react';
|
||||
import { SloSelector } from '../alerts/slo_selector';
|
||||
import type { EmbeddableProps } from './types';
|
||||
|
||||
interface Props {
|
||||
onCreate: (props: EmbeddableProps) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface SloConfig {
|
||||
sloId: string;
|
||||
sloInstanceId: string;
|
||||
}
|
||||
|
||||
export function Configuration({ onCreate, onCancel }: Props) {
|
||||
const [selectedSlo, setSelectedSlo] = useState<SloConfig>();
|
||||
const [duration, setDuration] = useState<string>('1h');
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const isDurationValid = duration.match(/^\d+[mhd]$/); // matches 1m, 78m, 1h, 6h, 1d, 24d
|
||||
|
||||
const isValid = !!selectedSlo && isDurationValid;
|
||||
|
||||
const onConfirmClick = () => {
|
||||
if (isValid) {
|
||||
onCreate({
|
||||
sloId: selectedSlo.sloId,
|
||||
sloInstanceId: selectedSlo.sloInstanceId,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onCancel} style={{ minWidth: 550 }}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.slo.burnRateEmbeddable.configuration.headerTitle', {
|
||||
defaultMessage: 'Burn rate configuration',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem grow>
|
||||
<SloSelector
|
||||
singleSelection={true}
|
||||
hasError={hasError}
|
||||
onSelected={(slo) => {
|
||||
setHasError(slo === undefined);
|
||||
if (slo && 'id' in slo) {
|
||||
setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId ?? ALL_VALUE });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={!isDurationValid}
|
||||
label={i18n.translate('xpack.slo.burnRateEmbeddable.configuration.durationLabel', {
|
||||
defaultMessage: 'Duration',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="sloConfigurationDuration"
|
||||
placeholder="1h"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
isInvalid={!isDurationValid}
|
||||
append={
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.slo.burnRateEmbeddable.configuration.durationTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Duration must be in the format of [value][unit], for example 5m, 3h, or 6d',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiButtonEmpty data-test-subj="sloConfigurationCancelButton" onClick={onCancel}>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.burnRateEmbeddable.configuration.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="sloConfigurationConfirmButton"
|
||||
isDisabled={!isValid || hasError}
|
||||
onClick={onConfirmClick}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.burnRateEmbeddable.configuration.cancelButtonLabel"
|
||||
defaultMessage="Confirm"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 const SLO_BURN_RATE_EMBEDDABLE_ID = 'SLO_BURN_RATE_EMBEDDABLE';
|
||||
export const ADD_BURN_RATE_ACTION_ID = 'CREATE_SLO_BURN_RATE_EMBEDDABLE';
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 type { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { SloPublicPluginsStart } from '../../..';
|
||||
import { Configuration } from './configuration';
|
||||
import type { EmbeddableProps, SloBurnRateEmbeddableState } from './types';
|
||||
|
||||
export async function openConfiguration(
|
||||
coreStart: CoreStart,
|
||||
pluginStart: SloPublicPluginsStart,
|
||||
initialState?: SloBurnRateEmbeddableState
|
||||
): Promise<EmbeddableProps> {
|
||||
const { overlays } = coreStart;
|
||||
const queryClient = new QueryClient();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const flyoutSession = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
...coreStart,
|
||||
...pluginStart,
|
||||
}}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Configuration
|
||||
onCreate={(update: EmbeddableProps) => {
|
||||
flyoutSession.close();
|
||||
resolve(update);
|
||||
}}
|
||||
onCancel={() => {
|
||||
flyoutSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>,
|
||||
coreStart
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 {
|
||||
ApplicationStart,
|
||||
IUiSettingsClient,
|
||||
NotificationsStart,
|
||||
type CoreStart,
|
||||
} from '@kbn/core/public';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
PublishesPanelTitle,
|
||||
PublishesWritablePanelTitle,
|
||||
SerializedTitles,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface EmbeddableProps {
|
||||
sloId: string;
|
||||
sloInstanceId: string;
|
||||
duration: string;
|
||||
reloadSubject?: Subject<boolean>;
|
||||
}
|
||||
|
||||
interface BurnRateCustomInput {
|
||||
sloId: string;
|
||||
sloInstanceId: string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
export type SloBurnRateEmbeddableState = SerializedTitles & BurnRateCustomInput;
|
||||
export type BurnRateApi = DefaultEmbeddableApi<SloBurnRateEmbeddableState> &
|
||||
PublishesWritablePanelTitle &
|
||||
PublishesPanelTitle;
|
||||
|
||||
export interface SloEmbeddableDeps {
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
i18n: CoreStart['i18n'];
|
||||
application: ApplicationStart;
|
||||
notifications: NotificationsStart;
|
||||
}
|
|
@ -41,7 +41,8 @@ export const sloKeys = {
|
|||
groups: () => [...sloKeys.all, 'group'] as const,
|
||||
overview: (filters: SLOOverviewFilter) => ['overview', filters] as const,
|
||||
details: () => [...sloKeys.all, 'details'] as const,
|
||||
detail: (sloId?: string) => [...sloKeys.details(), sloId] as const,
|
||||
detail: (sloId: string, instanceId: string | undefined, remoteName: string | undefined) =>
|
||||
[...sloKeys.details(), { sloId, instanceId, remoteName }] as const,
|
||||
rules: () => [...sloKeys.all, 'rules'] as const,
|
||||
rule: (sloIds: string[]) => [...sloKeys.rules(), sloIds] as const,
|
||||
activeAlerts: () => [...sloKeys.all, 'activeAlerts'] as const,
|
||||
|
|
|
@ -4,23 +4,19 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { ALL_VALUE, GetSLOBurnRatesResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import {
|
||||
QueryObserverResult,
|
||||
RefetchOptions,
|
||||
RefetchQueryFilters,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { ALL_VALUE, GetSLOBurnRatesResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { SLO_LONG_REFETCH_INTERVAL } from '../constants';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
import { SLO_LONG_REFETCH_INTERVAL } from '../constants';
|
||||
|
||||
export interface UseFetchSloBurnRatesResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: GetSLOBurnRatesResponse | undefined;
|
||||
refetch: <TPageData>(
|
||||
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
||||
|
@ -39,41 +35,35 @@ export function useFetchSloBurnRates({
|
|||
shouldRefetch,
|
||||
}: UseFetchSloBurnRatesParams): UseFetchSloBurnRatesResponse {
|
||||
const { http } = useKibana().services;
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
|
||||
{
|
||||
queryKey: sloKeys.burnRates(slo.id, slo.instanceId, windows),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.post<GetSLOBurnRatesResponse>(
|
||||
`/internal/observability/slos/${slo.id}/_burn_rates`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
windows,
|
||||
instanceId: slo.instanceId ?? ALL_VALUE,
|
||||
remoteName: slo.remote?.remoteName,
|
||||
}),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
const { isLoading, data, refetch } = useQuery({
|
||||
queryKey: sloKeys.burnRates(slo.id, slo.instanceId, windows),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.post<GetSLOBurnRatesResponse>(
|
||||
`/internal/observability/slos/${slo.id}/_burn_rates`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
windows,
|
||||
instanceId: slo.instanceId ?? ALL_VALUE,
|
||||
remoteName: slo.remote?.remoteName,
|
||||
}),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
},
|
||||
refetchInterval: shouldRefetch ? SLO_LONG_REFETCH_INTERVAL : undefined,
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
},
|
||||
refetchInterval: shouldRefetch ? SLO_LONG_REFETCH_INTERVAL : undefined,
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
refetch,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isInitialLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -36,14 +36,14 @@ export function useFetchSloDetails({
|
|||
}: {
|
||||
sloId?: string;
|
||||
instanceId?: string;
|
||||
remoteName?: string | null;
|
||||
remoteName?: string;
|
||||
shouldRefetch?: boolean;
|
||||
}): UseFetchSloDetailsResponse {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
|
||||
{
|
||||
queryKey: sloKeys.detail(sloId),
|
||||
queryKey: sloKeys.detail(sloId!, instanceId, remoteName),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.get<GetSLOResponse>(`/api/observability/slos/${sloId}`, {
|
||||
|
|
|
@ -52,7 +52,7 @@ export function useGetQueryParams() {
|
|||
|
||||
return {
|
||||
instanceId: !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined,
|
||||
remoteName,
|
||||
remoteName: remoteName !== null ? remoteName : undefined,
|
||||
isDeletingSlo: deleteSlo === 'true',
|
||||
removeDeleteQueryParam,
|
||||
isResettingSlo: resetSlo === 'true',
|
||||
|
|
|
@ -15,20 +15,21 @@ import {
|
|||
PluginInitializerContext,
|
||||
} from '@kbn/core/public';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
import { SloPublicPluginsSetup, SloPublicPluginsStart } from './types';
|
||||
import { PLUGIN_NAME, sloAppId } from '../common';
|
||||
import type { SloPublicSetup, SloPublicStart } from './types';
|
||||
import { ExperimentalFeatures, SloConfig } from '../common/config';
|
||||
import { SLOS_BASE_PATH } from '../common/locators/paths';
|
||||
import { SLO_ALERTS_EMBEDDABLE_ID } from './embeddable/slo/alerts/constants';
|
||||
import { SLO_BURN_RATE_EMBEDDABLE_ID } from './embeddable/slo/burn_rate/constants';
|
||||
import { SLO_ERROR_BUDGET_ID } from './embeddable/slo/error_budget/constants';
|
||||
import { SLO_OVERVIEW_EMBEDDABLE_ID } from './embeddable/slo/overview/constants';
|
||||
import { SloOverviewEmbeddableState } from './embeddable/slo/overview/types';
|
||||
import { SloDetailsLocatorDefinition } from './locators/slo_details';
|
||||
import { SloEditLocatorDefinition } from './locators/slo_edit';
|
||||
import { SloListLocatorDefinition } from './locators/slo_list';
|
||||
import { SLOS_BASE_PATH } from '../common/locators/paths';
|
||||
import { getCreateSLOFlyoutLazy } from './pages/slo_edit/shared_flyout/get_create_slo_flyout';
|
||||
import { registerBurnRateRuleType } from './rules/register_burn_rate_rule_type';
|
||||
import { ExperimentalFeatures, SloConfig } from '../common/config';
|
||||
import { SLO_OVERVIEW_EMBEDDABLE_ID } from './embeddable/slo/overview/constants';
|
||||
import { SloOverviewEmbeddableState } from './embeddable/slo/overview/types';
|
||||
import { SLO_ERROR_BUDGET_ID } from './embeddable/slo/error_budget/constants';
|
||||
import { SLO_ALERTS_EMBEDDABLE_ID } from './embeddable/slo/alerts/constants';
|
||||
import type { SloPublicSetup, SloPublicStart } from './types';
|
||||
import { SloPublicPluginsSetup, SloPublicPluginsStart } from './types';
|
||||
|
||||
export class SloPlugin
|
||||
implements Plugin<SloPublicSetup, SloPublicStart, SloPublicPluginsSetup, SloPublicPluginsStart>
|
||||
|
@ -95,6 +96,7 @@ export class SloPlugin
|
|||
const hasPlatinumLicense = license.hasAtLeast('platinum');
|
||||
if (hasPlatinumLicense) {
|
||||
const [coreStart, pluginsStart] = await coreSetup.getStartServices();
|
||||
|
||||
pluginsStart.dashboard.registerDashboardPanelPlacementSetting(
|
||||
SLO_OVERVIEW_EMBEDDABLE_ID,
|
||||
(serializedState: SloOverviewEmbeddableState | undefined) => {
|
||||
|
@ -104,6 +106,7 @@ export class SloPlugin
|
|||
return { width: 12, height: 8 };
|
||||
}
|
||||
);
|
||||
|
||||
pluginsSetup.embeddable.registerReactEmbeddableFactory(
|
||||
SLO_OVERVIEW_EMBEDDABLE_ID,
|
||||
async () => {
|
||||
|
@ -134,6 +137,24 @@ export class SloPlugin
|
|||
return getErrorBudgetEmbeddableFactory(deps);
|
||||
});
|
||||
|
||||
pluginsStart.dashboard.registerDashboardPanelPlacementSetting(
|
||||
SLO_BURN_RATE_EMBEDDABLE_ID,
|
||||
() => {
|
||||
return { width: 14, height: 7 };
|
||||
}
|
||||
);
|
||||
pluginsSetup.embeddable.registerReactEmbeddableFactory(
|
||||
SLO_BURN_RATE_EMBEDDABLE_ID,
|
||||
async () => {
|
||||
const deps = { ...coreStart, ...pluginsStart };
|
||||
|
||||
const { getBurnRateEmbeddableFactory } = await import(
|
||||
'./embeddable/slo/burn_rate/burn_rate_react_embeddable_factory'
|
||||
);
|
||||
return getBurnRateEmbeddableFactory(deps);
|
||||
}
|
||||
);
|
||||
|
||||
const registerAsyncSloUiActions = async () => {
|
||||
if (pluginsSetup.uiActions) {
|
||||
const { registerSloUiActions } = await import('./ui_actions');
|
||||
|
|
|
@ -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 type { CoreSetup } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
IncompatibleActionError,
|
||||
type UiActionsActionDefinition,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import { SloPublicPluginsStart, SloPublicStart } from '..';
|
||||
import {
|
||||
ADD_BURN_RATE_ACTION_ID,
|
||||
SLO_BURN_RATE_EMBEDDABLE_ID,
|
||||
} from '../embeddable/slo/burn_rate/constants';
|
||||
import { COMMON_SLO_GROUPING } from '../embeddable/slo/common/constants';
|
||||
|
||||
export function createBurnRatePanelAction(
|
||||
getStartServices: CoreSetup<SloPublicPluginsStart, SloPublicStart>['getStartServices']
|
||||
): UiActionsActionDefinition<EmbeddableApiContext> {
|
||||
return {
|
||||
id: ADD_BURN_RATE_ACTION_ID,
|
||||
grouping: COMMON_SLO_GROUPING,
|
||||
order: 30,
|
||||
getIconType: () => 'visGauge',
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
return apiIsPresentationContainer(embeddable);
|
||||
},
|
||||
execute: async ({ embeddable }) => {
|
||||
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
|
||||
const [coreStart, deps] = await getStartServices();
|
||||
try {
|
||||
const { openConfiguration } = await import(
|
||||
'../embeddable/slo/burn_rate/open_configuration'
|
||||
);
|
||||
const initialState = await openConfiguration(coreStart, deps);
|
||||
embeddable.addNewPanel(
|
||||
{
|
||||
panelType: SLO_BURN_RATE_EMBEDDABLE_ID,
|
||||
initialState,
|
||||
},
|
||||
true
|
||||
);
|
||||
} catch (e) {
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.slo.burnRateEmbeddable.ariaLabel', {
|
||||
defaultMessage: 'SLO Burn Rate',
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -11,6 +11,7 @@ import { createOverviewPanelAction } from './create_overview_panel_action';
|
|||
import { createAddErrorBudgetPanelAction } from './create_error_budget_action';
|
||||
import { createAddAlertsPanelAction } from './create_alerts_panel_action';
|
||||
import { SloPublicPluginsStart, SloPublicStart, SloPublicPluginsSetup } from '..';
|
||||
import { createBurnRatePanelAction } from './create_burn_rate_panel_action';
|
||||
|
||||
export function registerSloUiActions(
|
||||
core: CoreSetup<SloPublicPluginsStart, SloPublicStart>,
|
||||
|
@ -24,6 +25,7 @@ export function registerSloUiActions(
|
|||
const addOverviewPanelAction = createOverviewPanelAction(core.getStartServices);
|
||||
const addErrorBudgetPanelAction = createAddErrorBudgetPanelAction(core.getStartServices);
|
||||
const addAlertsPanelAction = createAddAlertsPanelAction(core.getStartServices);
|
||||
const addBurnRatePanelAction = createBurnRatePanelAction(core.getStartServices);
|
||||
|
||||
// Assign triggers
|
||||
// Only register these actions in stateful kibana, and the serverless observability project
|
||||
|
@ -31,5 +33,6 @@ export function registerSloUiActions(
|
|||
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addOverviewPanelAction);
|
||||
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addErrorBudgetPanelAction);
|
||||
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAlertsPanelAction);
|
||||
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addBurnRatePanelAction);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue