mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[SLO] error budget burn down chart embeddable (#176798)
Fixes https://github.com/elastic/kibana/issues/167572
- A new `SLO Error budget burn down` embeddable is added to the
Dashboard app
- A new `Attach to Dashboard` action is added to the Error budget burn
down chart in the SLO details page
- The selected SLO name is clickable and opens the SLO details page in a
Flyout
- The `Attach to Dashboard` action is hidden while on Dashboard app
9a1d257a
-0122-415f-ac5c-94c4aa0dff91
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
40d57754c8
commit
8f11edc616
19 changed files with 923 additions and 114 deletions
|
@ -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 React from 'react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { SloErrorBudgetEmbeddableInput, EmbeddableSloProps } from './types';
|
||||
|
||||
import { ObservabilityPublicPluginsStart } from '../../..';
|
||||
import { SloConfiguration } from './slo_configuration';
|
||||
export async function resolveEmbeddableSloUserInput(
|
||||
coreStart: CoreStart,
|
||||
pluginStart: ObservabilityPublicPluginsStart,
|
||||
input?: SloErrorBudgetEmbeddableInput
|
||||
): Promise<EmbeddableSloProps> {
|
||||
const { overlays } = coreStart;
|
||||
const queryClient = new QueryClient();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const modalSession = overlays.openModal(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
...coreStart,
|
||||
...pluginStart,
|
||||
}}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SloConfiguration
|
||||
onCreate={(update: EmbeddableSloProps) => {
|
||||
modalSession.close();
|
||||
resolve(update);
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>,
|
||||
{ i18n: coreStart.i18n, theme: coreStart.theme }
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { SloErrorBudgetEmbeddableFactoryDefinition } from './slo_error_budget_embeddable_factory';
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SloSelector } from '../alerts/slo_selector';
|
||||
import type { EmbeddableSloProps } from './types';
|
||||
|
||||
interface SloConfigurationProps {
|
||||
onCreate: (props: EmbeddableSloProps) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function SloConfiguration({ onCreate, onCancel }: SloConfigurationProps) {
|
||||
const [selectedSlo, setSelectedSlo] = useState<EmbeddableSloProps>();
|
||||
|
||||
const onConfirmClick = () =>
|
||||
onCreate({
|
||||
sloId: selectedSlo?.sloId,
|
||||
sloInstanceId: selectedSlo?.sloInstanceId,
|
||||
});
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onCancel} style={{ minWidth: 550 }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('xpack.observability.sloEmbeddable.config.sloSelector.headerTitle', {
|
||||
defaultMessage: 'SLO configuration',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow>
|
||||
<SloSelector
|
||||
singleSelection={true}
|
||||
hasError={hasError}
|
||||
onSelected={(slo) => {
|
||||
setHasError(slo === undefined);
|
||||
if (slo && 'id' in slo) {
|
||||
setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel} data-test-subj="sloCancelButton">
|
||||
<FormattedMessage
|
||||
id="xpack.observability.sloEmbeddable.config.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="sloConfirmButton"
|
||||
isDisabled={!selectedSlo || hasError}
|
||||
onClick={onConfirmClick}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.embeddableSlo.config.confirmButtonLabel"
|
||||
defaultMessage="Confirm configurations"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiLink, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useFetchSloDetails } from '../../../hooks/slo/use_fetch_slo_details';
|
||||
|
||||
import { ErrorBudgetChart } from '../../../pages/slo_details/components/error_budget_chart';
|
||||
import { EmbeddableSloProps } from './types';
|
||||
import { SloOverviewDetails } from '../common/slo_overview_details'; // TODO change to slo_details
|
||||
import { ErrorBudgetHeader } from '../../../pages/slo_details/components/error_budget_header';
|
||||
|
||||
export function SloErrorBudget({
|
||||
sloId,
|
||||
sloInstanceId,
|
||||
onRenderComplete,
|
||||
reloadSubject,
|
||||
}: EmbeddableSloProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedSlo, setSelectedSlo] = useState<SLOWithSummaryResponse | null>(null);
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
reloadSubject?.subscribe(() => {
|
||||
setLastRefreshTime(Date.now());
|
||||
});
|
||||
return () => {
|
||||
reloadSubject?.unsubscribe();
|
||||
};
|
||||
}, [reloadSubject]);
|
||||
|
||||
const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } =
|
||||
useFetchHistoricalSummary({
|
||||
list: [{ sloId: sloId!, instanceId: sloInstanceId ?? ALL_VALUE }],
|
||||
shouldRefetch: false,
|
||||
});
|
||||
|
||||
const sloHistoricalSummary = historicalSummaries.find(
|
||||
(historicalSummary) =>
|
||||
historicalSummary.sloId === sloId &&
|
||||
historicalSummary.instanceId === (sloInstanceId ?? ALL_VALUE)
|
||||
);
|
||||
|
||||
const errorBudgetBurnDownData = formatHistoricalData(
|
||||
sloHistoricalSummary?.data,
|
||||
'error_budget_remaining'
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
data: slo,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useFetchSloDetails({
|
||||
sloId,
|
||||
instanceId: sloInstanceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [lastRefreshTime, refetch]);
|
||||
useEffect(() => {
|
||||
if (!onRenderComplete) return;
|
||||
|
||||
if (!isLoading) {
|
||||
onRenderComplete();
|
||||
}
|
||||
}, [isLoading, onRenderComplete]);
|
||||
|
||||
const isSloNotFound = !isLoading && slo === undefined;
|
||||
|
||||
if (isRefetching || isLoading || !slo) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<LoadingContent>
|
||||
<EuiLoadingChart />
|
||||
</LoadingContent>
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSloNotFound) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<LoadingContent>
|
||||
{i18n.translate('xpack.observability.sloEmbeddable.overview.sloNotFoundText', {
|
||||
defaultMessage:
|
||||
'The SLO has been deleted. You can safely delete the widget from the dashboard.',
|
||||
})}
|
||||
</LoadingContent>
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
const hasGroupBy = slo.instanceId !== ALL_VALUE;
|
||||
return (
|
||||
<div ref={containerRef} style={{ width: '100%', padding: 10 }}>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{hasGroupBy ? (
|
||||
<EuiTitle size="xs">
|
||||
<h4>{slo.name}</h4>
|
||||
</EuiTitle>
|
||||
) : (
|
||||
<EuiLink
|
||||
css={{ fontSize: '16px' }}
|
||||
data-test-subj="o11ySloErrorBudgetLink"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
<h4>{slo.name}</h4>
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
{hasGroupBy && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="o11ySloErrorBudgetLink"
|
||||
onClick={() => {
|
||||
setSelectedSlo(slo);
|
||||
}}
|
||||
>
|
||||
{slo.groupBy}: {slo.instanceId}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<ErrorBudgetHeader showTitle={false} slo={slo} />
|
||||
<ErrorBudgetChart
|
||||
data={errorBudgetBurnDownData}
|
||||
isLoading={historicalSummaryLoading}
|
||||
slo={slo!}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<SloOverviewDetails slo={selectedSlo} setSelectedSlo={setSelectedSlo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const LoadingContainer = euiStyled.div`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const LoadingContent = euiStyled.div`
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
`;
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
Embeddable as AbstractEmbeddable,
|
||||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
type CoreStart,
|
||||
IUiSettingsClient,
|
||||
ApplicationStart,
|
||||
NotificationsStart,
|
||||
} from '@kbn/core/public';
|
||||
import { Subject } from 'rxjs';
|
||||
import type { SloErrorBudgetEmbeddableInput } from './types';
|
||||
import { SloErrorBudget } from './slo_error_budget_burn_down';
|
||||
export const SLO_ERROR_BUDGET_EMBEDDABLE = 'SLO_ERROR_BUDGET_EMBEDDABLE';
|
||||
|
||||
interface SloEmbeddableDeps {
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
i18n: CoreStart['i18n'];
|
||||
application: ApplicationStart;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
export class SLOErrorBudgetEmbeddable extends AbstractEmbeddable<
|
||||
SloErrorBudgetEmbeddableInput,
|
||||
EmbeddableOutput
|
||||
> {
|
||||
public readonly type = SLO_ERROR_BUDGET_EMBEDDABLE;
|
||||
private node?: HTMLElement;
|
||||
private reloadSubject: Subject<boolean>;
|
||||
|
||||
constructor(
|
||||
private readonly deps: SloEmbeddableDeps,
|
||||
initialInput: SloErrorBudgetEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
this.reloadSubject = new Subject<boolean>();
|
||||
|
||||
this.setTitle(
|
||||
this.input.title ||
|
||||
i18n.translate('xpack.observability.sloErrorBudgetEmbeddable.displayTitle', {
|
||||
defaultMessage: 'SLO Error Budget burn down',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.updateInput({ title });
|
||||
}
|
||||
|
||||
public onRenderComplete() {
|
||||
this.renderComplete.dispatchComplete();
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
super.render(node);
|
||||
this.node = node;
|
||||
// required for the export feature to work
|
||||
this.node.setAttribute('data-shared-item', '');
|
||||
|
||||
const { sloId, sloInstanceId } = this.getInput();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const I18nContext = this.deps.i18n.Context;
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<KibanaContextProvider services={this.deps}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SloErrorBudget sloId={sloId} sloInstanceId={sloInstanceId} />
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nContext>,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.reloadSubject.next(true);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 type { CoreSetup } from '@kbn/core/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryDefinition,
|
||||
ErrorEmbeddable,
|
||||
IContainer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { COMMON_SLO_GROUPING } from '../overview/slo_embeddable_factory';
|
||||
import {
|
||||
SLO_ERROR_BUDGET_EMBEDDABLE,
|
||||
SLOErrorBudgetEmbeddable,
|
||||
} from './slo_error_budget_embeddable';
|
||||
import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '../../..';
|
||||
import { SloErrorBudgetEmbeddableInput } from './types';
|
||||
|
||||
export type SloErrorBudgetEmbeddableFactory = EmbeddableFactory;
|
||||
export class SloErrorBudgetEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
|
||||
public readonly type = SLO_ERROR_BUDGET_EMBEDDABLE;
|
||||
|
||||
public readonly grouping = COMMON_SLO_GROUPING;
|
||||
|
||||
constructor(
|
||||
private getStartServices: CoreSetup<
|
||||
ObservabilityPublicPluginsStart,
|
||||
ObservabilityPublicStart
|
||||
>['getStartServices']
|
||||
) {}
|
||||
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getExplicitInput(): Promise<Partial<SloErrorBudgetEmbeddableInput>> {
|
||||
const [coreStart, pluginStart] = await this.getStartServices();
|
||||
try {
|
||||
const { resolveEmbeddableSloUserInput } = await import('./handle_explicit_input');
|
||||
return await resolveEmbeddableSloUserInput(coreStart, pluginStart);
|
||||
} catch (e) {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
public async create(initialInput: SloErrorBudgetEmbeddableInput, parent?: IContainer) {
|
||||
try {
|
||||
const [coreStart, pluginsStart] = await this.getStartServices();
|
||||
const deps = { ...coreStart, ...pluginsStart };
|
||||
return new SLOErrorBudgetEmbeddable(deps, initialInput, parent);
|
||||
} catch (e) {
|
||||
return new ErrorEmbeddable(e, initialInput, parent);
|
||||
}
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
return i18n.translate('xpack.observability.sloErrorBudgetEmbeddable.description', {
|
||||
defaultMessage: 'Get an error budget burn down chart of your SLOs',
|
||||
});
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.observability.sloErrorBudgetEmbeddable.displayName', {
|
||||
defaultMessage: 'SLO Error budget burn down',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'visLine';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface EmbeddableSloProps {
|
||||
sloId: string | undefined;
|
||||
sloInstanceId: string | undefined;
|
||||
reloadSubject?: Subject<boolean>;
|
||||
onRenderComplete?: () => void;
|
||||
}
|
||||
export type SloErrorBudgetEmbeddableInput = EmbeddableInput & EmbeddableSloProps;
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { SloOverviewDetails } from './slo_overview_details';
|
||||
import { SloOverviewDetails } from '../common/slo_overview_details';
|
||||
import { SloCardBadgesPortal } from '../../../pages/slos/components/card_view/badges_portal';
|
||||
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { MetricDatum } from '@elastic/charts/dist/chart_types/metric/specs';
|
||||
import { SloOverviewDetails } from './slo_overview_details';
|
||||
import { SloOverviewDetails } from '../common/slo_overview_details';
|
||||
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
|
||||
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiPopover, EuiContextMenuItem, EuiContextMenuPanel, EuiButtonIcon } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
setDashboardAttachmentReady?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function ErrorBudgetActions({ setDashboardAttachmentReady }: Props) {
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
|
||||
const handleAttachToDashboard = () => {
|
||||
setIsActionsPopoverOpen(false);
|
||||
if (setDashboardAttachmentReady) {
|
||||
setDashboardAttachmentReady(true);
|
||||
}
|
||||
};
|
||||
const ContextMenuButton = (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="o11yErrorBudgetActionsButton"
|
||||
iconType={'boxesHorizontal'}
|
||||
onClick={() => setIsActionsPopoverOpen(!isActionsPopoverOpen)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<EuiPopover
|
||||
isOpen={isActionsPopoverOpen}
|
||||
button={ContextMenuButton}
|
||||
closePopover={() => setIsActionsPopoverOpen(false)}
|
||||
>
|
||||
<EuiContextMenuPanel>
|
||||
<EuiContextMenuItem
|
||||
icon="dashboardApp"
|
||||
key="attachToDashboard"
|
||||
onClick={handleAttachToDashboard}
|
||||
data-test-subj="sloActinsAttachToDashboard"
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.item.actions.attachToDashboard', {
|
||||
defaultMessage: 'Attach to Dashboard',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
</EuiContextMenuPanel>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toDuration, toMinutes } from '../../../utils/slo/duration';
|
||||
import { ChartData } from '../../../typings/slo';
|
||||
import { WideChart } from './wide_chart';
|
||||
|
||||
function formatTime(minutes: number) {
|
||||
if (minutes > 59) {
|
||||
const mins = minutes % 60;
|
||||
const hours = (minutes - mins) / 60;
|
||||
return i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.minuteHoursLabel',
|
||||
{
|
||||
defaultMessage: '{hours}h {mins}m',
|
||||
values: { hours: Math.trunc(hours), mins: Math.trunc(mins) },
|
||||
}
|
||||
);
|
||||
}
|
||||
return i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.minuteLabel', {
|
||||
defaultMessage: '{minutes}m',
|
||||
values: { minutes },
|
||||
});
|
||||
}
|
||||
export interface Props {
|
||||
data: ChartData[];
|
||||
isLoading: boolean;
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function ErrorBudgetChart({ data, isLoading, slo }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
|
||||
let errorBudgetTimeRemainingFormatted;
|
||||
if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') {
|
||||
const totalSlices =
|
||||
toMinutes(toDuration(slo.timeWindow.duration)) /
|
||||
toMinutes(toDuration(slo.objective.timesliceWindow!));
|
||||
const errorBudgetRemainingInMinute =
|
||||
slo.summary.errorBudget.remaining * (slo.summary.errorBudget.initial * totalSlices);
|
||||
|
||||
errorBudgetTimeRemainingFormatted = formatTime(
|
||||
errorBudgetRemainingInMinute >= 0 ? errorBudgetRemainingInMinute : 0
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<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.observability.slo.sloDetails.errorBudgetChartPanel.remaining',
|
||||
{ defaultMessage: 'Remaining' }
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{errorBudgetTimeRemainingFormatted ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleColor={isSloFailed ? 'danger' : 'success'}
|
||||
title={errorBudgetTimeRemainingFormatted}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.remaining',
|
||||
{ defaultMessage: 'Remaining' }
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem>
|
||||
<WideChart
|
||||
chart="area"
|
||||
id={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.chartTitle',
|
||||
{
|
||||
defaultMessage: 'Error budget remaining',
|
||||
}
|
||||
)}
|
||||
state={isSloFailed ? 'error' : 'success'}
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,132 +5,106 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { EuiFlexGroup, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
LazySavedObjectSaveModalDashboard,
|
||||
withSuspense,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { toDuration, toMinutes } from '../../../utils/slo/duration';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public';
|
||||
import { ChartData } from '../../../typings/slo';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
|
||||
import { WideChart } from './wide_chart';
|
||||
|
||||
import { ErrorBudgetChart } from './error_budget_chart';
|
||||
import { ErrorBudgetHeader } from './error_budget_header';
|
||||
import { SLO_ERROR_BUDGET_EMBEDDABLE } from '../../../embeddable/slo/error_budget/slo_error_budget_embeddable';
|
||||
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
|
||||
export interface Props {
|
||||
data: ChartData[];
|
||||
isLoading: boolean;
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
function formatTime(minutes: number) {
|
||||
if (minutes > 59) {
|
||||
const mins = minutes % 60;
|
||||
const hours = (minutes - mins) / 60;
|
||||
return i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.minuteHoursLabel',
|
||||
{
|
||||
defaultMessage: '{hours}h {mins}m',
|
||||
values: { hours: Math.trunc(hours), mins: Math.trunc(mins) },
|
||||
}
|
||||
);
|
||||
}
|
||||
return i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.minuteLabel', {
|
||||
defaultMessage: '{minutes}m',
|
||||
values: { minutes },
|
||||
});
|
||||
}
|
||||
|
||||
export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
|
||||
const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED';
|
||||
const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false);
|
||||
const { embeddable } = useKibana().services;
|
||||
|
||||
let errorBudgetTimeRemainingFormatted;
|
||||
if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') {
|
||||
const totalSlices =
|
||||
toMinutes(toDuration(slo.timeWindow.duration)) /
|
||||
toMinutes(toDuration(slo.objective.timesliceWindow!));
|
||||
const errorBudgetRemainingInMinute =
|
||||
slo.summary.errorBudget.remaining * (slo.summary.errorBudget.initial * totalSlices);
|
||||
const handleAttachToDashboardSave: SaveModalDashboardProps['onSave'] = useCallback(
|
||||
({ dashboardId, newTitle, newDescription }) => {
|
||||
const stateTransfer = embeddable!.getStateTransfer();
|
||||
const embeddableInput = {
|
||||
title: newTitle,
|
||||
description: newDescription,
|
||||
sloId: slo.id,
|
||||
sloInstanceId: slo.instanceId,
|
||||
};
|
||||
|
||||
errorBudgetTimeRemainingFormatted = formatTime(
|
||||
errorBudgetRemainingInMinute >= 0 ? errorBudgetRemainingInMinute : 0
|
||||
);
|
||||
}
|
||||
const state = {
|
||||
input: embeddableInput,
|
||||
type: SLO_ERROR_BUDGET_EMBEDDABLE,
|
||||
};
|
||||
|
||||
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
|
||||
|
||||
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
|
||||
state,
|
||||
path,
|
||||
});
|
||||
},
|
||||
[embeddable, slo.id, slo.instanceId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="errorBudgetChartPanel">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.title', {
|
||||
defaultMessage: 'Error budget burn down',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
|
||||
? i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.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={numeral(slo.summary.errorBudget.remaining).format(percentFormat)}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.remaining',
|
||||
{ defaultMessage: 'Remaining' }
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{errorBudgetTimeRemainingFormatted ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleColor={isSloFailed ? 'danger' : 'success'}
|
||||
title={errorBudgetTimeRemainingFormatted}
|
||||
titleSize="s"
|
||||
description={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.remaining',
|
||||
{ defaultMessage: 'Remaining' }
|
||||
)}
|
||||
reverse
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem>
|
||||
<WideChart
|
||||
chart="area"
|
||||
id={i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.chartTitle',
|
||||
{
|
||||
defaultMessage: 'Error budget remaining',
|
||||
}
|
||||
)}
|
||||
state={isSloFailed ? 'error' : 'success'}
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
<>
|
||||
<EuiPanel
|
||||
paddingSize="m"
|
||||
color="transparent"
|
||||
hasBorder
|
||||
data-test-subj="errorBudgetChartPanel"
|
||||
onMouseOver={() => {
|
||||
if (!isMouseOver) {
|
||||
setIsMouseOver(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isMouseOver) {
|
||||
setIsMouseOver(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<ErrorBudgetHeader
|
||||
slo={slo}
|
||||
showTitle={true}
|
||||
isMouseOver={isMouseOver}
|
||||
setDashboardAttachmentReady={setDashboardAttachmentReady}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
||||
<ErrorBudgetChart slo={slo} data={data} isLoading={isLoading} />
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{isDashboardAttachmentReady ? (
|
||||
<SavedObjectSaveModalDashboard
|
||||
objectType={i18n.translate(
|
||||
'xpack.observability.slo.errorBudgetBurnDown.actions.attachToDashboard.objectTypeLabel',
|
||||
{ defaultMessage: 'SLO Error Budget burn down' }
|
||||
)}
|
||||
documentInfo={{
|
||||
title: i18n.translate(
|
||||
'xpack.observability.slo.errorBudgetBurnDown.actions.attachToDashboard.attachmentTitle',
|
||||
{ defaultMessage: 'SLO Error Budget burn down' }
|
||||
),
|
||||
}}
|
||||
canSaveByReference={false}
|
||||
onClose={() => {
|
||||
setDashboardAttachmentReady(false);
|
||||
}}
|
||||
onSave={handleAttachToDashboardSave}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import { buildSlo } from '../../../data/slo/slo';
|
||||
import { ErrorBudgetHeader } from './error_budget_header';
|
||||
|
||||
jest.mock('../../../utils/kibana_react');
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
|
||||
describe('In Observability Context', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
executionContext: {
|
||||
get: () => ({
|
||||
name: 'observability-overview',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('renders Error Budget Actions button on mouse over', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
render(<ErrorBudgetHeader isMouseOver={true} slo={slo} />);
|
||||
expect(screen.queryByTestId('o11yErrorBudgetActionsButton')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders "Error budget burn down" title', () => {
|
||||
const slo = buildSlo();
|
||||
render(<ErrorBudgetHeader isMouseOver={true} slo={slo} />);
|
||||
expect(screen.queryByTestId('errorBudgetPanelTitle')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('In Dashboard Context', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
executionContext: {
|
||||
get: () => ({
|
||||
name: 'dashboards',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('does not render Error budget Actions button on mouse over', async () => {
|
||||
const slo = buildSlo();
|
||||
render(<ErrorBudgetHeader isMouseOver={true} slo={slo} />);
|
||||
expect(screen.queryByTestId('o11yErrorBudgetActionsButton')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does not render the "Error budget burn down" title', () => {
|
||||
const slo = buildSlo();
|
||||
render(<ErrorBudgetHeader showTitle={false} isMouseOver={true} slo={slo} />);
|
||||
expect(screen.queryByTestId('errorBudgetPanelTitle')).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -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 React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
import { ErrorBudgetActions } from './error_budget_actions';
|
||||
|
||||
interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
showTitle?: boolean;
|
||||
isMouseOver?: boolean;
|
||||
setDashboardAttachmentReady?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function ErrorBudgetHeader({
|
||||
slo,
|
||||
showTitle = true,
|
||||
isMouseOver,
|
||||
setDashboardAttachmentReady,
|
||||
}: Props) {
|
||||
const { executionContext } = useKibana().services;
|
||||
const executionContextName = executionContext.get().name;
|
||||
const isDashboardContext = executionContextName === 'dashboards';
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
{showTitle && (
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" data-test-subj="errorBudgetPanelTitle">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.observability.slo.sloDetails.errorBudgetChartPanel.title',
|
||||
{
|
||||
defaultMessage: 'Error budget burn down',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!isDashboardContext && (
|
||||
<EuiFlexGroup justifyContent="flexEnd" wrap>
|
||||
{isMouseOver && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ErrorBudgetActions setDashboardAttachmentReady={setDashboardAttachmentReady} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
{rollingTimeWindowTypeSchema.is(slo.timeWindow.type)
|
||||
? i18n.translate('xpack.observability.slo.sloDetails.errorBudgetChartPanel.duration', {
|
||||
defaultMessage: 'Last {duration}',
|
||||
values: { duration: toDurationLabel(slo.timeWindow.duration) },
|
||||
})
|
||||
: toDurationAdverbLabel(slo.timeWindow.duration)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export function useErrorBudgetActions() {}
|
|
@ -102,6 +102,11 @@ const mockKibana = () => {
|
|||
return '';
|
||||
},
|
||||
},
|
||||
executionContext: {
|
||||
get: () => ({
|
||||
name: 'observability-overview',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -352,6 +352,15 @@ export class Plugin
|
|||
};
|
||||
registerSloAlertsEmbeddableFactory();
|
||||
|
||||
const registerSloErrorBudgetEmbeddableFactory = async () => {
|
||||
const { SloErrorBudgetEmbeddableFactoryDefinition } = await import(
|
||||
'./embeddable/slo/error_budget/slo_error_budget_embeddable_factory'
|
||||
);
|
||||
const factory = new SloErrorBudgetEmbeddableFactoryDefinition(coreSetup.getStartServices);
|
||||
pluginsSetup.embeddable.registerEmbeddableFactory(factory.type, factory);
|
||||
};
|
||||
registerSloErrorBudgetEmbeddableFactory();
|
||||
|
||||
const registerAsyncSloAlertsUiActions = async () => {
|
||||
if (pluginsSetup.uiActions) {
|
||||
const { registerSloAlertsUiActions } = await import('./ui_actions');
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
"@kbn/aiops-utils",
|
||||
"@kbn/event-annotation-common",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/core-lifecycle-browser"
|
||||
"@kbn/core-lifecycle-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue