Skip to content

Commit

Permalink
[SLO] create SLO embeddable widget (elastic#165949)
Browse files Browse the repository at this point in the history
Resolves elastic#165947
Resolves elastic/actionable-observability#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="https://github.com/elastic/kibana/assets/2852703/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 <[email protected]>
  • Loading branch information
mgiota and kibanamachine authored Sep 28, 2023
1 parent 5a785e8 commit 4c3fe71
Show file tree
Hide file tree
Showing 11 changed files with 604 additions and 1 deletion.
3 changes: 2 additions & 1 deletion x-pack/plugins/observability/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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';
}
}
Loading

0 comments on commit 4c3fe71

Please sign in to comment.