feat(slo): create burn rate embeddable (#189429)

This commit is contained in:
Kevin Delemme 2024-08-01 16:22:00 -04:00 committed by GitHub
parent 8481715534
commit e689da1bc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 739 additions and 49 deletions

View file

@ -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>
)
}
/>
);
}

View file

@ -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)

View file

@ -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;
`;

View file

@ -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;
};

View file

@ -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>
);
}

View file

@ -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';

View file

@ -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);
}
});
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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}`, {

View file

@ -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',

View file

@ -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');

View file

@ -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',
}),
};
}

View file

@ -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);
}
}