[SLO Embeddable rebuild] move error budget embeddable to the new react registry (#181532)

Fixes https://github.com/elastic/kibana/issues/179294

## 🍒 Summary
This PR converts the `SLO Error budget burn down` embeddable to the new
React Embeddable framework. There are no UI changes to the current
embeddable and the behavior should be the same. I have a video of how
the embeddable should work.


aaba8114-f74f-4dd7-8999-497bf9388562

## ✔️ Acceptance criteria
- A new `create_error_budget_action` action is created, which adds the
`SLO Error budget burn down` option in the `Add panel` section
- The `SLO Error budget burn down` menu option is grouped under `SLOs`
menu
- Clicking on the SLO name should open a `Flyout` with the SLO details
- `Within the Flyout` the `Add to Dashboard` option while hovering on
the Error Budget burn down should not be visible
- On the `SLO Details` page, the `Add to Dashboard` option while
hovering on the Error Budget burn down should be visible
- Kibana screenshot tool should report no timeout error. 
- Clicking on the `Refresh` button should reload the embeddable
- No reaction to the kql bar, datepicker or controls (New tickets will
cover these after the migration,
https://github.com/elastic/kibana/issues/181426 &
https://github.com/elastic/kibana/issues/181498)

<img width="600" alt="Screenshot 2024-04-24 at 12 20 18"
src="1c5562e8-3809-4688-b00f-b21a24cf2d1b">

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Panagiota Mitsopoulou 2024-04-29 20:59:22 +02:00 committed by GitHub
parent 5f4d863c62
commit 57a27278e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 223 additions and 208 deletions

View file

@ -5,4 +5,5 @@
* 2.0.
*/
export { SloErrorBudgetEmbeddableFactoryDefinition } from './slo_error_budget_embeddable_factory';
export const SLO_ERROR_BUDGET_ID = 'SLO_ERROR_BUDGET_EMBEDDABLE';
export const ADD_SLO_ERROR_BUDGET_ACTION_ID = 'CREATE_SLO_ERROR_BUDGET_EMBEDDABLE';

View file

@ -18,7 +18,7 @@ import { useFetchSloDetails } from '../../../hooks/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 { SloOverviewDetails } from '../common/slo_overview_details';
import { ErrorBudgetHeader } from '../../../pages/slo_details/components/error_budget_header';
import { SLOGroupings } from '../../../pages/slos/components/common/slo_groupings';
@ -111,7 +111,7 @@ export function SloErrorBudget({
}
const hasGroupBy = slo.instanceId !== ALL_VALUE;
return (
<div ref={containerRef} style={{ width: '100%', padding: 10 }}>
<div data-shared-item="" ref={containerRef} style={{ width: '100%', padding: 10 }}>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiLink

View file

@ -4,21 +4,18 @@
* 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 { toMountPoint } from '@kbn/react-kibana-mount';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { SloPublicPluginsStart } from '../../..';
import { SloConfiguration } from './slo_configuration';
export async function resolveEmbeddableSloUserInput(
import type { SloErrorBudgetEmbeddableState, EmbeddableSloProps } from './types';
export async function openSloConfiguration(
coreStart: CoreStart,
pluginStart: SloPublicPluginsStart,
input?: SloErrorBudgetEmbeddableInput
initialState?: SloErrorBudgetEmbeddableState
): Promise<EmbeddableSloProps> {
const { overlays } = coreStart;
const queryClient = new QueryClient();

View file

@ -0,0 +1,103 @@
/*
* 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_ERROR_BUDGET_ID } from './constants';
import { SloErrorBudgetEmbeddableState, SloEmbeddableDeps, ErrorBudgetApi } from './types';
import { SloErrorBudget } from './error_budget_burn_down';
export const getErrorBudgetPanelTitle = () =>
i18n.translate('xpack.slo.errorBudgetEmbeddable.title', {
defaultMessage: 'SLO Error Budget burn down',
});
const queryClient = new QueryClient();
export const getErrorBudgetEmbeddableFactory = (deps: SloEmbeddableDeps) => {
const factory: ReactEmbeddableFactory<SloErrorBudgetEmbeddableState, ErrorBudgetApi> = {
type: SLO_ERROR_BUDGET_ID,
deserializeState: (state) => {
return state.rawState as SloErrorBudgetEmbeddableState;
},
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const defaultTitle$ = new BehaviorSubject<string | undefined>(getErrorBudgetPanelTitle());
const sloId$ = new BehaviorSubject(state.sloId);
const sloInstanceId$ = new BehaviorSubject(state.sloInstanceId);
const reload$ = new Subject<boolean>();
const api = buildApi(
{
...titlesApi,
defaultPanelTitle: defaultTitle$,
serializeState: () => {
return {
rawState: {
...serializeTitles(),
sloId: sloId$.getValue(),
sloInstanceId: sloInstanceId$.getValue(),
},
};
},
},
{
sloId: [sloId$, (value) => sloId$.next(value)],
sloInstanceId: [sloInstanceId$, (value) => sloInstanceId$.next(value)],
...titleComparators,
}
);
const fetchSubscription = fetch$(api)
.pipe()
.subscribe((next) => {
reload$.next(next.isReload);
});
return {
api,
Component: () => {
const [sloId, sloInstanceId] = useBatchedPublishingSubjects(sloId$, sloInstanceId$);
const I18nContext = deps.i18n.Context;
useEffect(() => {
return () => {
fetchSubscription.unsubscribe();
};
}, []);
return (
<I18nContext>
<Router history={createBrowserHistory()}>
<KibanaContextProvider services={deps}>
<QueryClientProvider client={queryClient}>
<SloErrorBudget
sloId={sloId}
sloInstanceId={sloInstanceId}
reloadSubject={reload$}
/>
</QueryClientProvider>
</KibanaContextProvider>
</Router>
</I18nContext>
);
},
};
},
};
return factory;
};

View file

@ -19,8 +19,8 @@ import {
} 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';
import { SloSelector } from '../alerts/slo_selector';
interface SloConfigurationProps {
onCreate: (props: EmbeddableSloProps) => void;
@ -29,14 +29,13 @@ interface SloConfigurationProps {
export function SloConfiguration({ onCreate, onCancel }: SloConfigurationProps) {
const [selectedSlo, setSelectedSlo] = useState<EmbeddableSloProps>();
const [hasError, setHasError] = useState(false);
const onConfirmClick = () =>
onCreate({
sloId: selectedSlo?.sloId,
sloInstanceId: selectedSlo?.sloInstanceId,
});
const [hasError, setHasError] = useState(false);
return (
<EuiModal onClose={onCancel} style={{ minWidth: 550 }}>
<EuiModalHeader>

View file

@ -1,104 +0,0 @@
/*
* 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 { Router } from '@kbn/shared-ux-router';
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 { createBrowserHistory } from 'history';
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.slo.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>
<Router history={createBrowserHistory()}>
<KibanaContextProvider services={this.deps}>
<QueryClientProvider client={queryClient}>
<SloErrorBudget sloId={sloId} sloInstanceId={sloInstanceId} />
</QueryClientProvider>
</KibanaContextProvider>
</Router>
</I18nContext>,
node
);
}
public reload() {
this.reloadSubject.next(true);
}
public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -1,73 +0,0 @@
/*
* 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 { SloPublicPluginsStart, SloPublicStart } 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<SloPublicPluginsStart, SloPublicStart>['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.slo.sloErrorBudgetEmbeddable.description', {
defaultMessage: 'Get an error budget burn down chart of your SLOs',
});
}
public getDisplayName() {
return i18n.translate('xpack.slo.sloErrorBudgetEmbeddable.displayName', {
defaultMessage: 'SLO Error budget burn down',
});
}
public getIconType() {
return 'visLine';
}
}

View file

@ -4,8 +4,19 @@
* 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 {
SerializedTitles,
PublishesWritablePanelTitle,
PublishesPanelTitle,
} from '@kbn/presentation-publishing';
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
import { Subject } from 'rxjs';
import {
type CoreStart,
IUiSettingsClient,
ApplicationStart,
NotificationsStart,
} from '@kbn/core/public';
export interface EmbeddableSloProps {
sloId: string | undefined;
@ -13,4 +24,21 @@ export interface EmbeddableSloProps {
reloadSubject?: Subject<boolean>;
onRenderComplete?: () => void;
}
export type SloErrorBudgetEmbeddableInput = EmbeddableInput & EmbeddableSloProps;
interface ErrorBudgetCustomInput {
sloId: string | undefined;
sloInstanceId: string | undefined;
}
export type SloErrorBudgetEmbeddableState = SerializedTitles & ErrorBudgetCustomInput;
export type ErrorBudgetApi = DefaultEmbeddableApi<SloErrorBudgetEmbeddableState> &
PublishesWritablePanelTitle &
PublishesPanelTitle;
export interface SloEmbeddableDeps {
uiSettings: IUiSettingsClient;
http: CoreStart['http'];
i18n: CoreStart['i18n'];
application: ApplicationStart;
notifications: NotificationsStart;
}

View file

@ -18,7 +18,7 @@ import { useKibana } from '../../../utils/kibana_react';
import { ChartData } from '../../../typings/slo';
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';
import { SLO_ERROR_BUDGET_ID } from '../../../embeddable/slo/error_budget/constants';
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
export interface Props {
data: ChartData[];
@ -44,7 +44,7 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) {
const state = {
input: embeddableInput,
type: SLO_ERROR_BUDGET_EMBEDDABLE,
type: SLO_ERROR_BUDGET_ID,
};
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;

View file

@ -15,6 +15,7 @@ import {
PluginInitializerContext,
} from '@kbn/core/public';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { registerReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { SloPublicPluginsSetup, SloPublicPluginsStart } from './types';
import { PLUGIN_NAME, sloAppId } from '../common';
import type { SloPublicSetup, SloPublicStart } from './types';
@ -25,6 +26,7 @@ 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_ERROR_BUDGET_ID } from './embeddable/slo/error_budget/constants';
export class SloPlugin
implements Plugin<SloPublicSetup, SloPublicStart, SloPublicPluginsSetup, SloPublicPluginsStart>
@ -111,22 +113,24 @@ export class SloPlugin
};
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();
registerReactEmbeddableFactory(SLO_ERROR_BUDGET_ID, async () => {
const [coreStart, pluginsStart] = await coreSetup.getStartServices();
const registerAsyncSloAlertsUiActions = async () => {
const deps = { ...coreStart, ...pluginsStart };
const { getErrorBudgetEmbeddableFactory } = await import(
'./embeddable/slo/error_budget/error_budget_react_embeddable_factory'
);
return getErrorBudgetEmbeddableFactory(deps);
});
const registerAsyncSloUiActions = async () => {
if (pluginsSetup.uiActions) {
const { registerSloAlertsUiActions } = await import('./ui_actions');
registerSloAlertsUiActions(pluginsSetup.uiActions, coreSetup);
const { registerSloUiActions } = await import('./ui_actions');
registerSloUiActions(pluginsSetup.uiActions, coreSetup);
}
};
registerAsyncSloAlertsUiActions();
registerAsyncSloUiActions();
}
};
assertPlatinumLicense();

View file

@ -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 { i18n } from '@kbn/i18n';
import type { CoreSetup } from '@kbn/core/public';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import {
IncompatibleActionError,
type UiActionsActionDefinition,
} from '@kbn/ui-actions-plugin/public';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import {
ADD_SLO_ERROR_BUDGET_ACTION_ID,
SLO_ERROR_BUDGET_ID,
} from '../embeddable/slo/error_budget/constants';
import { SloPublicPluginsStart, SloPublicStart } from '..';
import { COMMON_SLO_GROUPING } from '../embeddable/slo/overview/slo_embeddable_factory';
export function createAddErrorBudgetPanelAction(
getStartServices: CoreSetup<SloPublicPluginsStart, SloPublicStart>['getStartServices']
): UiActionsActionDefinition<EmbeddableApiContext> {
return {
id: ADD_SLO_ERROR_BUDGET_ACTION_ID,
grouping: COMMON_SLO_GROUPING,
getIconType: () => 'visLine',
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);
},
execute: async ({ embeddable }) => {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
const [coreStart, deps] = await getStartServices();
try {
const { openSloConfiguration } = await import(
'../embeddable/slo/error_budget/error_budget_open_configuration'
);
const initialState = await openSloConfiguration(coreStart, deps);
embeddable.addNewPanel(
{
panelType: SLO_ERROR_BUDGET_ID,
initialState,
},
true
);
} catch (e) {
return Promise.reject();
}
},
getDisplayName: () =>
i18n.translate('xpack.slo.errorBudgetEmbeddable.ariaLabel', {
defaultMessage: 'SLO Error Budget',
}),
};
}

View file

@ -10,16 +10,20 @@ import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import type { CoreSetup } from '@kbn/core/public';
import { createEditSloAlertsPanelAction } from './edit_slo_alerts_panel';
import { createEditSloOverviewPanelAction } from './edit_slo_overview_panel';
import { createAddErrorBudgetPanelAction } from './create_error_budget_action';
import { SloPublicPluginsStart, SloPublicStart } from '..';
export function registerSloAlertsUiActions(
export function registerSloUiActions(
uiActions: UiActionsSetup,
core: CoreSetup<SloPublicPluginsStart, SloPublicStart>
) {
// Initialize actions
const editSloAlertsPanelAction = createEditSloAlertsPanelAction(core.getStartServices);
const editSloOverviewPanelAction = createEditSloOverviewPanelAction(core.getStartServices);
const addErrorBudgetPanelAction = createAddErrorBudgetPanelAction(core.getStartServices);
// Assign triggers
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSloAlertsPanelAction);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSloOverviewPanelAction);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addErrorBudgetPanelAction);
}

View file

@ -97,5 +97,6 @@
"@kbn/data-view-field-editor-plugin",
"@kbn/securitysolution-io-ts-utils",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/presentation-containers",
]
}