mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[SLO] create SLO embeddable widget (#165949)
Resolves https://github.com/elastic/kibana/issues/165947
Resolves https://github.com/elastic/actionable-observability/issues/124
### Summary
This PR adds an Embeddable SLO Overview Widget to the Dashboard app. It
uses a [Metric
chart](https://elastic.github.io/elastic-charts/?path=/story/metric-alpha--basic)
component and displays an overview of the SLO health:
- name
- current sli value
- target
- status (background color)
### ✔️ Acceptance criteria
- The SLO widget should display the basic information listed above
- The SLO widget should be clickable and lead to the slo detail page
- The user should be able to select the SLO and filter to instanceId
- The tag "url.domain:mail.co" is the partition field and instanceId
value
<img width="1189" alt="Screenshot 2023-09-21 at 21 07 23"
src="03539b9d
-23a5-45eb-aafb-df42e9421f77">
For more information regarding the key concepts and the usage of an
embeddable you can have a look at the Embeddable plugin
[README](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5a785e8a41
commit
4c3fe71821
11 changed files with 604 additions and 1 deletions
|
@ -30,7 +30,8 @@
|
|||
"security",
|
||||
"share",
|
||||
"unifiedSearch",
|
||||
"visualizations"
|
||||
"visualizations",
|
||||
"dashboard",
|
||||
],
|
||||
"optionalPlugins": ["discover", "home", "licensing", "usageCollection", "cloud", "spaces"],
|
||||
"requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat", "stackAlerts", "spaces"],
|
||||
|
|
|
@ -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 { EmbeddableSloProps, SloEmbeddableInput } from './types';
|
||||
|
||||
import { ObservabilityPublicPluginsStart } from '../../..';
|
||||
import { SloConfiguration } from './slo_configuration';
|
||||
export async function resolveEmbeddableSloUserInput(
|
||||
coreStart: CoreStart,
|
||||
pluginStart: ObservabilityPublicPluginsStart,
|
||||
input?: SloEmbeddableInput
|
||||
): 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 { SloOverviewEmbeddableFactoryDefinition } from './slo_embeddable_factory';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 './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
|
||||
hasError={hasError}
|
||||
onSelected={(slo) => {
|
||||
if (slo === undefined) {
|
||||
setHasError(true);
|
||||
} else {
|
||||
setHasError(false);
|
||||
}
|
||||
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,96 @@
|
|||
/*
|
||||
* 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 { Subscription } from 'rxjs';
|
||||
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 } from '@kbn/core/public';
|
||||
import { SloOverview } from './slo_overview';
|
||||
import type { SloEmbeddableInput } from './types';
|
||||
|
||||
export const SLO_EMBEDDABLE = 'SLO_EMBEDDABLE';
|
||||
|
||||
interface SloEmbeddableDeps {
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
i18n: CoreStart['i18n'];
|
||||
application: ApplicationStart;
|
||||
}
|
||||
|
||||
export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, EmbeddableOutput> {
|
||||
public readonly type = SLO_EMBEDDABLE;
|
||||
private subscription: Subscription;
|
||||
private node?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private readonly deps: SloEmbeddableDeps,
|
||||
initialInput: SloEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
|
||||
this.subscription = new Subscription();
|
||||
this.subscription.add(this.getInput$().subscribe(() => this.reload()));
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.updateInput({ title });
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
this.node = node;
|
||||
this.setTitle(
|
||||
this.input.title ||
|
||||
i18n.translate('xpack.observability.sloEmbeddable.displayTitle', {
|
||||
defaultMessage: 'SLO Overview',
|
||||
})
|
||||
);
|
||||
this.input.lastReloadRequestTime = Date.now();
|
||||
|
||||
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}>
|
||||
<SloOverview
|
||||
sloId={sloId}
|
||||
sloInstanceId={sloInstanceId}
|
||||
lastReloadRequestTime={this.input.lastReloadRequestTime}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nContext>,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
public reload() {
|
||||
if (this.node) {
|
||||
this.render(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.subscription.unsubscribe();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 {
|
||||
IContainer,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableFactory,
|
||||
ErrorEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { SLOEmbeddable, SLO_EMBEDDABLE } from './slo_embeddable';
|
||||
import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '../../..';
|
||||
import type { SloEmbeddableInput } from './types';
|
||||
|
||||
export type SloOverviewEmbeddableFactory = EmbeddableFactory;
|
||||
export class SloOverviewEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
|
||||
public readonly type = SLO_EMBEDDABLE;
|
||||
|
||||
constructor(
|
||||
private getStartServices: CoreSetup<
|
||||
ObservabilityPublicPluginsStart,
|
||||
ObservabilityPublicStart
|
||||
>['getStartServices']
|
||||
) {}
|
||||
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getExplicitInput(): Promise<Partial<SloEmbeddableInput>> {
|
||||
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: SloEmbeddableInput, parent?: IContainer) {
|
||||
try {
|
||||
const [{ uiSettings, application, http, i18n: i18nService }] = await this.getStartServices();
|
||||
return new SLOEmbeddable(
|
||||
{ uiSettings, application, http, i18n: i18nService },
|
||||
initialInput,
|
||||
parent
|
||||
);
|
||||
} catch (e) {
|
||||
return new ErrorEmbeddable(e, initialInput, parent);
|
||||
}
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
return i18n.translate('xpack.observability.sloEmbeddable.description', {
|
||||
defaultMessage: 'Get an overview of your SLO health',
|
||||
});
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.observability.sloEmbeddable.displayName', {
|
||||
defaultMessage: 'SLO Overview',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'visGauge';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiIcon, useEuiBackgroundColor } from '@elastic/eui';
|
||||
import { Chart, Metric, MetricTrendShape, Settings } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useFetchSloDetails } from '../../../hooks/slo/use_fetch_slo_details';
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
|
||||
import { EmbeddableSloProps } from './types';
|
||||
|
||||
export function SloOverview({ sloId, sloInstanceId, lastReloadRequestTime }: EmbeddableSloProps) {
|
||||
const {
|
||||
uiSettings,
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
const { isLoading, slo, refetch, isRefetching } = useFetchSloDetails({
|
||||
sloId,
|
||||
instanceId: sloInstanceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [lastReloadRequestTime, refetch]);
|
||||
|
||||
const percentFormat = uiSettings.get('format:percent:defaultPattern');
|
||||
const isSloNotFound = !isLoading && slo === undefined;
|
||||
|
||||
const getIcon = useCallback(
|
||||
(type: string) =>
|
||||
({ width = 20, height = 20, color }: { width: number; height: number; color: string }) => {
|
||||
return <EuiIcon type={type} width={width} height={height} fill={color} />;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const sloSummary = slo?.summary;
|
||||
const sloStatus = sloSummary?.status;
|
||||
const healthyColor = useEuiBackgroundColor('success');
|
||||
const noDataColor = useEuiBackgroundColor('subdued');
|
||||
const degradingColor = useEuiBackgroundColor('warning');
|
||||
const violatedColor = useEuiBackgroundColor('danger');
|
||||
let color;
|
||||
switch (sloStatus) {
|
||||
case 'HEALTHY':
|
||||
color = healthyColor;
|
||||
break;
|
||||
case 'NO_DATA':
|
||||
color = noDataColor;
|
||||
break;
|
||||
case 'DEGRADING':
|
||||
color = degradingColor;
|
||||
break;
|
||||
case 'VIOLATED':
|
||||
color = violatedColor;
|
||||
break;
|
||||
default:
|
||||
color = noDataColor;
|
||||
}
|
||||
|
||||
if (isRefetching || isLoading) {
|
||||
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 TargetCopy = i18n.translate('xpack.observability.sloEmbeddable.overview.sloTargetLabel', {
|
||||
defaultMessage: 'Target',
|
||||
});
|
||||
const extraContent = `${TargetCopy} <b>${numeral(slo?.objective.target).format(
|
||||
percentFormat
|
||||
)}</b>`;
|
||||
// eslint-disable-next-line react/no-danger
|
||||
const extra = <span dangerouslySetInnerHTML={{ __html: extraContent }} />;
|
||||
const metricData =
|
||||
slo !== undefined
|
||||
? [
|
||||
{
|
||||
color,
|
||||
title: slo.name,
|
||||
subtitle: slo.groupBy !== ALL_VALUE ? `${slo.groupBy}:${slo.instanceId}` : '',
|
||||
icon: getIcon('visGauge'),
|
||||
value:
|
||||
sloStatus === 'NO_DATA'
|
||||
? NOT_AVAILABLE_LABEL
|
||||
: numeral(slo.summary.sliValue).format(percentFormat),
|
||||
valueFormatter: (value: number) => `${value}%`,
|
||||
extra,
|
||||
trend: [],
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return (
|
||||
<>
|
||||
<Chart>
|
||||
<Settings
|
||||
onElementClick={() => {
|
||||
navigateToUrl(
|
||||
basePath.prepend(
|
||||
paths.observability.sloDetails(
|
||||
slo!.id,
|
||||
slo?.groupBy !== ALL_VALUE && slo?.instanceId ? slo.instanceId : undefined
|
||||
)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Metric id={`${slo?.id}-${slo?.instanceId}`} data={[metricData]} />
|
||||
</Chart>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,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 React, { useEffect, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { debounce } from 'lodash';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
|
||||
|
||||
interface Props {
|
||||
initialSlo?: SLOWithSummaryResponse;
|
||||
onSelected: (slo: SLOWithSummaryResponse | undefined) => void;
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
||||
const SLO_REQUIRED = i18n.translate('xpack.observability.sloEmbeddable.config.errors.sloRequired', {
|
||||
defaultMessage: 'SLO is required.',
|
||||
});
|
||||
|
||||
export function SloSelector({ initialSlo, onSelected, hasError }: Props) {
|
||||
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>();
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const { isInitialLoading, isLoading, sloList } = useFetchSloList({
|
||||
kqlQuery: `slo.name: ${searchValue.replaceAll(' ', '*')}*`,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isLoadedWithData = !isLoading && sloList!.results !== undefined;
|
||||
const opts: Array<EuiComboBoxOptionOption<string>> = isLoadedWithData
|
||||
? sloList!.results!.map((slo) => {
|
||||
const label =
|
||||
slo.instanceId !== ALL_VALUE
|
||||
? `${slo.name} (${slo.groupBy}: ${slo.instanceId})`
|
||||
: slo.name;
|
||||
return {
|
||||
value: `${slo.id}-${slo.instanceId}`,
|
||||
label,
|
||||
instanceId: slo.instanceId,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
setOptions(opts);
|
||||
}, [isLoading, sloList]);
|
||||
|
||||
const onChange = (opts: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedOptions(opts);
|
||||
const selectedSlo =
|
||||
opts.length === 1
|
||||
? sloList!.results?.find((slo) => opts[0].value === `${slo.id}-${slo.instanceId}`)
|
||||
: undefined;
|
||||
|
||||
onSelected(selectedSlo);
|
||||
};
|
||||
|
||||
const onSearchChange = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
if (isInitialLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow fullWidth isInvalid={hasError} error={hasError ? SLO_REQUIRED : undefined}>
|
||||
<EuiComboBox
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observability.sloEmbeddable.config.sloSelector.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'SLO',
|
||||
}
|
||||
)}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.sloEmbeddable.config.sloSelector.placeholder',
|
||||
{
|
||||
defaultMessage: 'Select a SLO',
|
||||
}
|
||||
)}
|
||||
data-test-subj="sloSelector"
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
async
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
onSearchChange={onSearchChange}
|
||||
isInvalid={hasError}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export interface EmbeddableSloProps {
|
||||
sloId: string | undefined;
|
||||
sloInstanceId: string | undefined;
|
||||
lastReloadRequestTime?: number | undefined;
|
||||
}
|
||||
|
||||
export type SloEmbeddableInput = EmbeddableInput & EmbeddableSloProps;
|
|
@ -56,6 +56,7 @@ import {
|
|||
ObservabilityAIAssistantPluginSetup,
|
||||
ObservabilityAIAssistantPluginStart,
|
||||
} from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import { AiopsPluginStart } from '@kbn/aiops-plugin/public/types';
|
||||
import { RulesLocatorDefinition } from './locators/rules';
|
||||
import { RuleDetailsLocatorDefinition } from './locators/rule_details';
|
||||
|
@ -111,6 +112,7 @@ export interface ObservabilityPublicPluginsSetup {
|
|||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||
home?: HomePublicPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
}
|
||||
|
||||
export interface ObservabilityPublicPluginsStart {
|
||||
|
@ -286,6 +288,14 @@ export class Plugin
|
|||
coreSetup.application.register(app);
|
||||
|
||||
registerObservabilityRuleTypes(config, this.observabilityRuleTypeRegistry);
|
||||
const registerSloEmbeddableFactory = async () => {
|
||||
const { SloOverviewEmbeddableFactoryDefinition } = await import(
|
||||
'./embeddable/slo/overview/slo_embeddable_factory'
|
||||
);
|
||||
const factory = new SloOverviewEmbeddableFactoryDefinition(coreSetup.getStartServices);
|
||||
pluginsSetup.embeddable.registerEmbeddableFactory(factory.type, factory);
|
||||
};
|
||||
registerSloEmbeddableFactory();
|
||||
|
||||
if (pluginsSetup.home) {
|
||||
pluginsSetup.home.featureCatalogue.registerSolution({
|
||||
|
|
|
@ -84,6 +84,8 @@
|
|||
"@kbn/core-capabilities-common",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/osquery-plugin",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/aiops-plugin",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/deeplinks-observability",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue