diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index 96bfbb4738b2e..75c4e164c298a 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -20,6 +20,7 @@ "dataViews", "dataViewEditor", "embeddable", + "uiActions", "exploratoryView", "features", "files", diff --git a/x-pack/plugins/observability/public/components/alerts_table/render_cell_value.tsx b/x-pack/plugins/observability/public/components/alerts_table/render_cell_value.tsx index 03551ca514946..258a15d6721be 100644 --- a/x-pack/plugins/observability/public/components/alerts_table/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/components/alerts_table/render_cell_value.tsx @@ -47,12 +47,16 @@ const getRenderValue = (mappedNonEcsValue: any) => { if (!isEmpty(value)) { if (typeof value === 'object') { - return JSON.stringify(value); + try { + return JSON.stringify(value); + } catch (e) { + return 'Error: Unable to parse JSON value.'; + } } return value; } - return '—'; + return '—-'; }; /** diff --git a/x-pack/plugins/observability/public/components/alerts_table/slo/default_columns.tsx b/x-pack/plugins/observability/public/components/alerts_table/slo/default_columns.tsx new file mode 100644 index 0000000000000..4872070a6c39c --- /dev/null +++ b/x-pack/plugins/observability/public/components/alerts_table/slo/default_columns.tsx @@ -0,0 +1,60 @@ +/* + * 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. + */ + +/** + * We need to produce types and code transpilation at different folders during the build of the package. + * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. + * This way plugins can do targeted imports to reduce the final code bundle + */ +import { ALERT_DURATION, ALERT_REASON, ALERT_STATUS, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; +import { EuiDataGridColumn } from '@elastic/eui'; +import type { ColumnHeaderOptions } from '@kbn/timelines-plugin/common'; +import { i18n } from '@kbn/i18n'; + +/** + * columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface, + * plus additional TGrid column properties + */ +export const columns: Array< + Pick & ColumnHeaderOptions +> = [ + { + columnHeaderType: 'not-filtered', + displayAsText: i18n.translate( + 'xpack.observability.sloAlertsEmbeddable.alertsTGrid.statusColumnDescription', + { + defaultMessage: 'Status', + } + ), + id: ALERT_STATUS, + initialWidth: 110, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: i18n.translate('xpack.observability.alertsTGrid.durationColumnDescription', { + defaultMessage: 'Duration', + }), + id: ALERT_DURATION, + initialWidth: 116, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: i18n.translate('xpack.observability.alertsTGrid.sloColumnDescription', { + defaultMessage: 'Rule name', + }), + id: ALERT_RULE_NAME, + initialWidth: 110, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonColumnDescription', { + defaultMessage: 'Reason', + }), + id: ALERT_REASON, + linkField: '*', + }, +]; diff --git a/x-pack/plugins/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx b/x-pack/plugins/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx new file mode 100644 index 0000000000000..d43c12c4627ed --- /dev/null +++ b/x-pack/plugins/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public'; +import { TIMESTAMP } from '@kbn/rule-data-utils'; +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { casesFeatureId, observabilityFeatureId } from '../../../../common'; +import { getRenderCellValue } from './render_cell_value'; +import { columns } from './default_columns'; +import { useGetAlertFlyoutComponents } from '../../alerts_flyout/use_get_alert_flyout_components'; +import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; +import type { ConfigSchema } from '../../../plugin'; +import type { TopAlert } from '../../../typings/alerts'; +import { SLO_ALERTS_TABLE_CONFID } from '../../../embeddable/slo/constants'; + +export const getSloAlertsTableConfiguration = ( + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, + config: ConfigSchema +): AlertsTableConfigurationRegistry => ({ + id: SLO_ALERTS_TABLE_CONFID, + cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, + columns, + getRenderCellValue: (({ setFlyoutAlert }: { setFlyoutAlert: (data: TopAlert) => void }) => { + return getRenderCellValue({ observabilityRuleTypeRegistry, setFlyoutAlert }); + }) as unknown as GetRenderCellValue, + sort: [ + { + [TIMESTAMP]: { + order: 'desc' as SortOrder, + }, + }, + ], + + useInternalFlyout: () => { + const { header, body, footer } = useGetAlertFlyoutComponents(observabilityRuleTypeRegistry); + return { header, body, footer }; + }, +}); diff --git a/x-pack/plugins/observability/public/components/alerts_table/slo/render_cell_value.tsx b/x-pack/plugins/observability/public/components/alerts_table/slo/render_cell_value.tsx new file mode 100644 index 0000000000000..a7c5f7c71d535 --- /dev/null +++ b/x-pack/plugins/observability/public/components/alerts_table/slo/render_cell_value.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { + ALERT_DURATION, + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, + ALERT_REASON, + TIMESTAMP, +} from '@kbn/rule-data-utils'; +import { isEmpty } from 'lodash'; +import type { + DeprecatedCellValueElementProps, + TimelineNonEcsData, +} from '@kbn/timelines-plugin/common'; + +import { asDuration } from '../../../../common/utils/formatters'; +import { AlertStatusIndicator } from '../../alert_status_indicator'; +import { TimestampTooltip } from '../timestamp_tooltip'; +import { parseAlert } from '../../../pages/alerts/helpers/parse_alert'; +import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; +import type { TopAlert } from '../../../typings/alerts'; + +export const getMappedNonEcsValue = ({ + data, + fieldName, +}: { + data: TimelineNonEcsData[]; + fieldName: string; +}): string[] | undefined => { + const item = data.find((d) => d.field === fieldName); + if (item != null && item.value != null) { + return item.value; + } + return undefined; +}; + +const getRenderValue = (mappedNonEcsValue: any) => { + // can be updated when working on https://github.com/elastic/kibana/issues/140819 + const value = Array.isArray(mappedNonEcsValue) ? mappedNonEcsValue.join() : mappedNonEcsValue; + + if (!isEmpty(value)) { + if (typeof value === 'object') { + return JSON.stringify(value); + } + return value; + } + + return '—'; +}; + +/** + * This implementation of `EuiDataGrid`'s `renderCellValue` + * accepts `EuiDataGridCellValueElementProps`, plus `data` + * from the TGrid + */ + +export const getRenderCellValue = ({ + setFlyoutAlert, + observabilityRuleTypeRegistry, +}: { + setFlyoutAlert: (data: TopAlert) => void; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; +}) => { + return ({ columnId, data }: DeprecatedCellValueElementProps) => { + if (!data) return null; + const mappedNonEcsValue = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + const value = getRenderValue(mappedNonEcsValue); + + switch (columnId) { + case ALERT_STATUS: + if (value !== ALERT_STATUS_ACTIVE && value !== ALERT_STATUS_RECOVERED) { + // NOTE: This should only be needed to narrow down the type. + // Status should be either "active" or "recovered". + return null; + } + return ; + case TIMESTAMP: + return ; + case ALERT_DURATION: + return asDuration(Number(value)); + case ALERT_RULE_NAME: + return value; + case ALERT_REASON: + const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); + const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); + + return ( + setFlyoutAlert && setFlyoutAlert(alert)} + > + {alert.reason} + + ); + default: + return <>{value}; + } + }; +}; diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx new file mode 100644 index 0000000000000..21cf577c00dc1 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx @@ -0,0 +1,77 @@ +/* + * 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, { useMemo } from 'react'; +import type { TimeRange } from '@kbn/es-query'; +import { useSloAlertsQuery } from './slo_alerts_table'; +import { getAlertSummaryTimeRange } from '../../../../utils/alert_summary_widget'; +import { observabilityAlertFeatureIds } from '../../../../../common/constants'; +import { useTimeBuckets } from '../../../../hooks/use_time_buckets'; +import { calculateTimeRangeBucketSize } from '../../../../pages/overview/helpers/calculate_bucket_size'; +import { SloEmbeddableDeps } from '../slo_alerts_embeddable'; +import { SloItem } from '../types'; + +const DEFAULT_INTERVAL = '60s'; +const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm'; + +interface Props { + deps: SloEmbeddableDeps; + slos: SloItem[]; + timeRange: TimeRange; + onLoaded?: () => void; +} + +export function SloAlertsSummary({ slos, deps, timeRange, onLoaded }: Props) { + const { + charts, + triggersActionsUi: { getAlertSummaryWidget: AlertSummaryWidget }, + } = deps; + + const esQuery = useSloAlertsQuery(slos, timeRange); + const timeBuckets = useTimeBuckets(); + const bucketSize = useMemo( + () => + calculateTimeRangeBucketSize( + { + from: timeRange.from, + to: timeRange.to, + }, + timeBuckets + ), + [timeRange.from, timeRange.to, timeBuckets] + ); + const alertSummaryTimeRange = useMemo( + () => + getAlertSummaryTimeRange( + { + from: timeRange.from, + to: timeRange.to, + }, + bucketSize?.intervalString ?? DEFAULT_INTERVAL, + bucketSize?.dateFormat ?? DEFAULT_DATE_FORMAT + ), + [timeRange.from, timeRange.to, bucketSize] + ); + + const chartProps = { + theme: charts.theme.useChartsTheme(), + baseTheme: charts.theme.useChartsBaseTheme(), + }; + return ( + { + if (onLoaded) { + onLoaded(); + } + }} + /> + ); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_table.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_table.tsx new file mode 100644 index 0000000000000..9df76482e4f39 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/components/slo_alerts_table.tsx @@ -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 React, { useMemo } from 'react'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import type { TimeRange } from '@kbn/es-query'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import { AlertsTableStateProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_table/alerts_table_state'; +import { SloEmbeddableDeps } from '../slo_alerts_embeddable'; +import type { SloItem } from '../types'; +import { SLO_ALERTS_TABLE_CONFID } from '../../constants'; + +const ALERTS_PER_PAGE = 10; +const ALERTS_TABLE_ID = 'xpack.observability.sloAlertsEmbeddable.alert.table'; + +interface Props { + deps: SloEmbeddableDeps; + slos: SloItem[]; + timeRange: TimeRange; + onLoaded?: () => void; + lastReloadRequestTime: number | undefined; +} + +export const getSloInstanceFilter = (sloId: string, sloInstanceId: string) => { + return { + bool: { + must: [ + { + term: { + 'slo.id': sloId, + }, + }, + ...(sloInstanceId !== ALL_VALUE + ? [ + { + term: { + 'slo.instanceId': sloInstanceId, + }, + }, + ] + : []), + ], + }, + }; +}; + +export const useSloAlertsQuery = (slos: SloItem[], timeRange: TimeRange) => { + return useMemo(() => { + const query: AlertsTableStateProps['query'] = { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: timeRange.from, + }, + }, + }, + { + term: { + 'kibana.alert.rule.rule_type_id': 'slo.rules.burnRate', + }, + }, + { + bool: { + should: slos.map((slo) => getSloInstanceFilter(slo.id, slo.instanceId)), + }, + }, + ], + }, + }; + + return query; + }, [slos, timeRange]); +}; + +export function SloAlertsTable({ slos, deps, timeRange, onLoaded, lastReloadRequestTime }: Props) { + const { + triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable }, + } = deps; + + return ( + { + if (onLoaded) { + onLoaded(); + } + }} + lastReloadRequestTime={lastReloadRequestTime} + /> + ); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/handle_explicit_input.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/handle_explicit_input.tsx new file mode 100644 index 0000000000000..7f246adf679aa --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/handle_explicit_input.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import 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, SloAlertsEmbeddableInput } from './types'; + +import { ObservabilityPublicPluginsStart } from '../../..'; +import { SloConfiguration } from './slo_configuration'; +export async function resolveEmbeddableSloUserInput( + coreStart: CoreStart, + pluginStart: ObservabilityPublicPluginsStart, + input?: SloAlertsEmbeddableInput +): Promise { + const { overlays } = coreStart; + const queryClient = new QueryClient(); + return new Promise(async (resolve, reject) => { + try { + const modalSession = overlays.openModal( + toMountPoint( + + + { + modalSession.close(); + resolve(update); + }} + onCancel={() => { + modalSession.close(); + // @ts-expect-error + resolve(undefined); + }} + /> + + , + { i18n: coreStart.i18n, theme: coreStart.theme } + ) + ); + } catch (error) { + reject(error); + } + }); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/index.ts b/x-pack/plugins/observability/public/embeddable/slo/alerts/index.ts new file mode 100644 index 0000000000000..9c4ec3cc57d71 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/index.ts @@ -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 { SloAlertsEmbeddableFactoryDefinition } from './slo_alerts_embeddable_factory'; diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable.tsx new file mode 100644 index 0000000000000..ab488f58802d9 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { i18n } from '@kbn/i18n'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { createBrowserHistory } from 'history'; +import { + Embeddable as AbstractEmbeddable, + EmbeddableOutput, + IContainer, +} from '@kbn/embeddable-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { CasesUiStart } from '@kbn/cases-plugin/public'; + +import { + type CoreStart, + IUiSettingsClient, + ApplicationStart, + NotificationsStart, +} from '@kbn/core/public'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { Router } from '@kbn/shared-ux-router'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; +import { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; + +import { Subject, Subscription } from 'rxjs'; +import { SloAlertsWrapper } from './slo_alerts_wrapper'; +import type { SloAlertsEmbeddableInput } from './types'; +export const SLO_ALERTS_EMBEDDABLE = 'SLO_ALERTS_EMBEDDABLE'; + +const history = createBrowserHistory(); + +export interface SloEmbeddableDeps { + uiSettings: IUiSettingsClient; + http: CoreStart['http']; + i18n: CoreStart['i18n']; + application: ApplicationStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + data: DataPublicPluginStart; + notifications: NotificationsStart; + cases: CasesUiStart; + settings: SettingsStart; + security: SecurityPluginStart; + charts: ChartsPluginStart; + uiActions: UiActionsStart; + serverless?: ServerlessPluginStart; +} + +export class SLOAlertsEmbeddable extends AbstractEmbeddable< + SloAlertsEmbeddableInput, + EmbeddableOutput +> { + public readonly type = SLO_ALERTS_EMBEDDABLE; + private reloadSubject: Subject; + private node?: HTMLElement; + kibanaVersion: string; + private subscription: Subscription; + + constructor( + private readonly deps: SloEmbeddableDeps, + initialInput: SloAlertsEmbeddableInput, + kibanaVersion: string, + parent?: IContainer + ) { + super(initialInput, {}, parent); + this.deps = deps; + this.kibanaVersion = kibanaVersion; + this.reloadSubject = new Subject(); + + this.subscription = this.getInput$().subscribe((input) => { + this.reloadSubject.next(input); + }); + + this.setTitle( + this.input.title || + i18n.translate('xpack.observability.sloAlertsEmbeddable.displayTitle', { + defaultMessage: 'SLO Alerts', + }) + ); + } + public onRenderComplete() { + this.renderComplete.dispatchComplete(); + } + + setTitle(title: string) { + this.updateInput({ title }); + } + + public render(node: HTMLElement) { + super.render(node); + this.node = node; + // required for the export feature to work + this.node.setAttribute('data-shared-item', ''); + + const queryClient = new QueryClient(); + + const I18nContext = this.deps.i18n.Context; + const { slos, timeRange = { from: 'now-15m/m', to: 'now' } } = this.getInput(); + + const deps = this.deps; + const kibanaVersion = this.kibanaVersion; + ReactDOM.render( + + + + + this.onRenderComplete()} + embeddable={this} + deps={deps} + slos={slos} + timeRange={timeRange} + reloadSubject={this.reloadSubject} + /> + + + + , + node + ); + } + + public reload() { + this.reloadSubject?.next(undefined); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.ts b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.ts new file mode 100644 index 0000000000000..dcdd632d6edb0 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.ts @@ -0,0 +1,74 @@ +/* + * 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_ALERTS_EMBEDDABLE, SLOAlertsEmbeddable } from './slo_alerts_embeddable'; +import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '../../..'; +import { SloAlertsEmbeddableInput } from './types'; + +export type SloAlertsEmbeddableFactory = EmbeddableFactory; +export class SloAlertsEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { + public readonly type = SLO_ALERTS_EMBEDDABLE; + + public readonly grouping = COMMON_SLO_GROUPING; + + constructor( + private getStartServices: CoreSetup< + ObservabilityPublicPluginsStart, + ObservabilityPublicStart + >['getStartServices'], + private kibanaVersion: string + ) {} + + public async isEditable() { + return true; + } + + public async getExplicitInput(): Promise> { + 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: SloAlertsEmbeddableInput, parent?: IContainer) { + try { + const [coreStart, pluginsStart] = await this.getStartServices(); + const deps = { ...coreStart, ...pluginsStart }; + return new SLOAlertsEmbeddable(deps, initialInput, this.kibanaVersion, parent); + } catch (e) { + return new ErrorEmbeddable(e, initialInput, parent); + } + } + + public getDescription() { + return i18n.translate('xpack.observability.sloAlertsEmbeddable.description', { + defaultMessage: 'Get an overview of your SLO alerts', + }); + } + + public getDisplayName() { + return i18n.translate('xpack.observability.sloAlertsEmbeddable.displayName', { + defaultMessage: 'SLO Alerts', + }); + } + + public getIconType() { + return 'alert'; + } +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx new file mode 100644 index 0000000000000..2038c5ed707da --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx @@ -0,0 +1,164 @@ +/* + * 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, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import type { TimeRange } from '@kbn/es-query'; +import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { IEmbeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public'; +import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import { Subject } from 'rxjs'; +import styled from 'styled-components'; +import { SloAlertsSummary } from './components/slo_alerts_summary'; +import { SloAlertsTable } from './components/slo_alerts_table'; +import type { SloItem } from './types'; +import { SloEmbeddableDeps } from './slo_alerts_embeddable'; +import { SloAlertsEmbeddableInput } from './types'; +import { EDIT_SLO_ALERTS_ACTION } from '../../../ui_actions/edit_slo_alerts_panel'; +import { paths } from '../../../../common/locators/paths'; + +interface Props { + deps: SloEmbeddableDeps; + slos: SloItem[]; + timeRange: TimeRange; + embeddable: IEmbeddable; + onRenderComplete?: () => void; + reloadSubject: Subject; +} + +export function SloAlertsWrapper({ + embeddable, + slos: initialSlos, + deps, + timeRange: initialTimeRange, + onRenderComplete, + reloadSubject, +}: Props) { + const { + application: { navigateToUrl }, + http: { basePath }, + } = deps; + + const [timeRange, setTimeRange] = useState(initialTimeRange); + const [slos, setSlos] = useState(initialSlos); + + const [lastRefreshTime, setLastRefreshTime] = useState(undefined); + + useEffect(() => { + const subs = reloadSubject?.subscribe((input) => { + if (input) { + const { timeRange: nTimeRange, slos: nSlos } = input; + + setSlos(nSlos); + + if (nTimeRange && (nTimeRange.from !== timeRange.from || nTimeRange.to !== timeRange.to)) { + setTimeRange(nTimeRange); + } + } + setLastRefreshTime(Date.now()); + }); + return () => { + subs?.unsubscribe(); + }; + }, [reloadSubject, timeRange.from, timeRange.to]); + + useEffect(() => { + setTimeRange(initialTimeRange); + }, [initialTimeRange]); + + const [isSummaryLoaded, setIsSummaryLoaded] = useState(false); + const [isTableLoaded, setIsTableLoaded] = useState(false); + useEffect(() => { + if (!onRenderComplete) { + return; + } + if (isSummaryLoaded && isTableLoaded) { + onRenderComplete(); + } + }, [isSummaryLoaded, isTableLoaded, onRenderComplete]); + const handleGoToAlertsClick = () => { + let kuery = ''; + slos.map((slo, index) => { + const shouldAddOr = index < slos.length - 1; + kuery += `(slo.id:"${slo.id}" and slo.instanceId:"${slo.instanceId}")`; + if (shouldAddOr) { + kuery += ' or '; + } + }); + navigateToUrl( + `${basePath.prepend(paths.observability.alerts)}?_a=(kuery:'${kuery}',rangeFrom:${ + timeRange.from + },rangeTo:${timeRange.to})` + ); + }; + + return ( + + + + { + const trigger = deps.uiActions.getTrigger(CONTEXT_MENU_TRIGGER); + deps.uiActions.getAction(EDIT_SLO_ALERTS_ACTION).execute({ + trigger, + embeddable, + } as ActionExecutionContext); + }} + data-test-subj="o11ySloAlertsWrapperSlOsIncludedLink" + > + {i18n.translate('xpack.observability.sloAlertsWrapper.sLOsIncludedFlexItemLabel', { + defaultMessage: '{numOfSlos} SLOs included', + values: { numOfSlos: slos.length }, + })} + + + + + + + + + + + + setIsSummaryLoaded(true)} + /> + + + setIsTableLoaded(true)} + lastReloadRequestTime={lastRefreshTime} + /> + + + + ); +} + +const Wrapper = styled.div` + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_configuration.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_configuration.tsx new file mode 100644 index 0000000000000..148ffd8492ff2 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_configuration.tsx @@ -0,0 +1,97 @@ +/* + * 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, SloAlertsEmbeddableInput, SloItem } from './types'; + +interface SloConfigurationProps { + initialInput?: Partial; + onCreate: (props: EmbeddableSloProps) => void; // TODO check change point detection + onCancel: () => void; +} + +export function SloConfiguration({ initialInput, onCreate, onCancel }: SloConfigurationProps) { + const [selectedSlos, setSelectedSlos] = useState(initialInput?.slos ?? []); + const [hasError, setHasError] = useState(false); + const onConfirmClick = () => onCreate({ slos: selectedSlos }); + + return ( + + + + {i18n.translate('xpack.observability.sloEmbeddable.config.sloSelector.headerTitle', { + defaultMessage: 'SLO configuration', + })} + + + + + + { + setHasError(slos === undefined); + if (Array.isArray(slos)) { + setSelectedSlos( + slos?.map((slo) => ({ + id: slo?.id, + instanceId: slo?.instanceId, + name: slo?.name, + groupBy: slo?.groupBy, + })) as SloItem[] + ); + } + }} + /> + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_selector.tsx similarity index 63% rename from x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx rename to x-pack/plugins/observability/public/embeddable/slo/alerts/slo_selector.tsx index 8ad4985e0096d..dd926cc9fc179 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/slo_selector.tsx @@ -11,55 +11,54 @@ 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'; +import { SloItem } from './types'; interface Props { - initialSlo?: SLOWithSummaryResponse; - onSelected: (slo: SLOWithSummaryResponse | undefined) => void; + initialSlos?: SloItem[]; + onSelected: (slos: SLOWithSummaryResponse[] | SLOWithSummaryResponse | undefined) => void; hasError?: boolean; + singleSelection?: boolean; } const SLO_REQUIRED = i18n.translate('xpack.observability.sloEmbeddable.config.errors.sloRequired', { defaultMessage: 'SLO is required.', }); -export function SloSelector({ initialSlo, onSelected, hasError }: Props) { +export function SloSelector({ initialSlos, onSelected, hasError, singleSelection }: Props) { + const mapSlosToOptions = (slos: SloItem[] | SLOWithSummaryResponse[] | undefined) => + slos?.map((slo) => ({ + label: + slo.instanceId !== ALL_VALUE ? `${slo.name} (${slo.groupBy}: ${slo.instanceId})` : slo.name, + value: `${slo.id}-${slo.instanceId}`, + })) ?? []; const [options, setOptions] = useState>>([]); - const [selectedOptions, setSelectedOptions] = useState>>(); + const [selectedOptions, setSelectedOptions] = useState>>( + mapSlosToOptions(initialSlos) + ); const [searchValue, setSearchValue] = useState(''); - const { - isInitialLoading, - isLoading, - data: sloList, - } = useFetchSloList({ - kqlQuery: `slo.name: ${searchValue.replaceAll(' ', '*')}*`, + const query = `${searchValue.replaceAll(' ', '*')}*`; + const { isLoading, data: sloList } = useFetchSloList({ + kqlQuery: `slo.name: ${query} or slo.instanceId: ${query}`, + perPage: 100, }); useEffect(() => { - const isLoadedWithData = !isLoading && sloList!.results !== undefined; + const isLoadedWithData = !isLoading && sloList?.results !== undefined; const opts: Array> = 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, - }; - }) + ? mapSlosToOptions(sloList?.results) : []; setOptions(opts); }, [isLoading, sloList]); const onChange = (opts: Array>) => { setSelectedOptions(opts); - const selectedSlo = - opts.length === 1 - ? sloList!.results?.find((slo) => opts[0].value === `${slo.id}-${slo.instanceId}`) + const selectedSlos = + opts.length >= 1 + ? sloList!.results?.filter((slo) => + opts.find((opt) => opt.value === `${slo.id}-${slo.instanceId}`) + ) : undefined; - - onSelected(selectedSlo); + onSelected(singleSelection ? selectedSlos?.[0] : selectedSlos); }; const onSearchChange = useMemo( @@ -70,10 +69,6 @@ export function SloSelector({ initialSlo, onSelected, hasError }: Props) { [] ); - if (isInitialLoading) { - return null; - } - return ( ); diff --git a/x-pack/plugins/observability/public/embeddable/slo/alerts/types.ts b/x-pack/plugins/observability/public/embeddable/slo/alerts/types.ts new file mode 100644 index 0000000000000..02480110560a5 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/alerts/types.ts @@ -0,0 +1,23 @@ +/* + * 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 type { TimeRange } from '@kbn/es-query'; + +export interface SloItem { + id: string; + instanceId: string; + name: string; + groupBy: string; +} + +export interface EmbeddableSloProps { + slos: SloItem[]; + timeRange?: TimeRange; + lastReloadRequestTime?: number | undefined; +} + +export type SloAlertsEmbeddableInput = EmbeddableInput & EmbeddableSloProps; diff --git a/x-pack/plugins/observability/public/embeddable/slo/constants.ts b/x-pack/plugins/observability/public/embeddable/slo/constants.ts new file mode 100644 index 0000000000000..00f4b9858988f --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 { AlertConsumers } from '@kbn/rule-data-utils'; +export const SLO_ALERTS_EMBEDDABLE = 'SLO_ALERTS_EMBEDDABLE'; + +export const SLO_ALERTS_TABLE_CONFID = `${AlertConsumers.SLO}-embeddable-alerts-table`; diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx index c7fcb63be7a3a..eeee1d708463b 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { SloSelector } from './slo_selector'; +import { SloSelector } from '../alerts/slo_selector'; import type { EmbeddableSloProps } from './types'; interface SloConfigurationProps { @@ -46,14 +46,13 @@ export function SloConfiguration({ onCreate, onCancel }: SloConfigurationProps) { - if (slo === undefined) { - setHasError(true); - } else { - setHasError(false); + setHasError(slo === undefined); + if (slo && 'id' in slo) { + setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId }); } - setSelectedSlo({ sloId: slo?.id, sloInstanceId: slo?.instanceId }); }} /> diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx index ec5709ce45f98..94f80e30fc582 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; import ReactDOM from 'react-dom'; -import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { @@ -18,6 +17,7 @@ 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 { Subject } from 'rxjs'; import { SloOverview } from './slo_overview'; import type { SloEmbeddableInput } from './types'; @@ -32,8 +32,8 @@ interface SloEmbeddableDeps { export class SLOEmbeddable extends AbstractEmbeddable { public readonly type = SLO_EMBEDDABLE; - private subscription: Subscription; private node?: HTMLElement; + private reloadSubject: Subject; constructor( private readonly deps: SloEmbeddableDeps, @@ -41,19 +41,20 @@ export class SLOEmbeddable extends AbstractEmbeddable(); - this.subscription = new Subscription(); - this.subscription.add(this.getInput$().subscribe(() => this.reload())); + this.setTitle( + this.input.title || + i18n.translate('xpack.observability.sloEmbeddable.displayTitle', { + defaultMessage: 'SLO Overview', + }) + ); } setTitle(title: string) { this.updateInput({ title }); } - public reportsEmbeddableLoad() { - return true; - } - public onRenderComplete() { this.renderComplete.dispatchComplete(); } @@ -63,13 +64,6 @@ export class SLOEmbeddable extends AbstractEmbeddable this.onRenderComplete()} sloId={sloId} sloInstanceId={sloInstanceId} - lastReloadRequestTime={this.input.lastReloadRequestTime} + reloadSubject={this.reloadSubject} /> @@ -93,14 +87,11 @@ export class SLOEmbeddable extends AbstractEmbeddable 'SLOs', + getIconType: () => { + return 'visGauge'; + }, + }, +]; export type SloOverviewEmbeddableFactory = EmbeddableFactory; export class SloOverviewEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition, IProvidesPanelPlacementSettings { public readonly type = SLO_EMBEDDABLE; + public readonly grouping = COMMON_SLO_GROUPING; + constructor( private getStartServices: CoreSetup< ObservabilityPublicPluginsStart, @@ -49,9 +60,7 @@ export class SloOverviewEmbeddableFactoryDefinition SloEmbeddableInput, unknown >['getPanelPlacementSettings'] = () => { - const width = 8; - const height = 7; - return { width, height, strategy: 'placeAtTop' }; + return { width: 12, height: 8 }; }; public async create(initialInput: SloEmbeddableInput, parent?: IContainer) { diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx index 873c37f89eca2..0bd1b7048dcc0 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx @@ -5,32 +5,41 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useEffect, useState, useRef } 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 { ALL_VALUE } from '@kbn/slo-schema'; +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'; +import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; +import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo'; +import { SloCardItemBadges } from '../../../pages/slos/components/card_view/slo_card_item_badges'; +import { SloCardChart } from '../../../pages/slos/components/card_view/slo_card_item'; 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, onRenderComplete, + reloadSubject, }: EmbeddableSloProps) { - const { - uiSettings, - application: { navigateToUrl }, - http: { basePath }, - } = useKibana().services; + const containerRef = useRef(null); + + const [lastRefreshTime, setLastRefreshTime] = useState(undefined); + + useEffect(() => { + reloadSubject?.subscribe(() => { + setLastRefreshTime(Date.now()); + }); + return () => { + reloadSubject?.unsubscribe(); + }; + }, [reloadSubject]); + const { isLoading, data: slo, @@ -41,9 +50,21 @@ export function SloOverview({ instanceId: sloInstanceId, }); + const { data: rulesBySlo } = useFetchRulesForSlo({ + sloIds: sloId ? [sloId] : [], + }); + + const { data: activeAlertsBySlo } = useFetchActiveAlerts({ + sloIdsAndInstanceIds: slo ? [[slo.id, slo.instanceId ?? ALL_VALUE] as [string, string]] : [], + }); + + const { data: historicalSummaries = [] } = useFetchHistoricalSummary({ + list: slo ? [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }] : [], + }); + useEffect(() => { refetch(); - }, [lastReloadRequestTime, refetch]); + }, [lastRefreshTime, refetch]); useEffect(() => { if (!onRenderComplete) return; @@ -52,42 +73,9 @@ export function SloOverview({ } }, [isLoading, onRenderComplete]); - 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 ; - }, - [] - ); - - 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) { + if (isRefetching || isLoading || !slo) { return ( @@ -110,52 +98,30 @@ export function SloOverview({ ); } - const TargetCopy = i18n.translate('xpack.observability.sloEmbeddable.overview.sloTargetLabel', { - defaultMessage: 'Target', - }); - const extraContent = `${TargetCopy} ${numeral(slo?.objective.target).format( - percentFormat - )}`; - // eslint-disable-next-line react/no-danger - const extra = ; - 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, - }, - ] - : []; + const rules = rulesBySlo?.[slo?.id]; + const activeAlerts = activeAlertsBySlo.get(slo); + + const hasGroupBy = Boolean(slo.groupBy && slo.groupBy !== ALL_VALUE); + + const historicalSummary = historicalSummaries.find( + (histSummary) => + histSummary.sloId === slo.id && histSummary.instanceId === (slo.instanceId ?? ALL_VALUE) + )?.data; + + const historicalSliData = formatHistoricalData(historicalSummary, 'sli_value'); + return ( - <> - - { - navigateToUrl( - basePath.prepend( - paths.observability.sloDetails( - slo!.id, - slo?.groupBy !== ALL_VALUE && slo?.instanceId ? slo.instanceId : undefined - ) - ) - ); - }} - locale={i18n.getLocale()} +
+ + + - - - + +
); } diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts b/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts index a6f55069dc47e..f4ba87fd7d52f 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts @@ -5,11 +5,12 @@ * 2.0. */ import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { Subject } from 'rxjs'; export interface EmbeddableSloProps { sloId: string | undefined; sloInstanceId: string | undefined; - lastReloadRequestTime?: number | undefined; + reloadSubject?: Subject; onRenderComplete?: () => void; } diff --git a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts index 5eaee397b9ab7..8642cda0669c1 100644 --- a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { ActiveAlerts, UseFetchActiveAlerts } from '../use_fetch_active_alerts'; +import { UseFetchActiveAlerts } from '../use_fetch_active_alerts'; +import { ActiveAlerts } from '../active_alerts'; export const useFetchActiveAlerts = ({ sloIdsAndInstanceIds = [], diff --git a/x-pack/plugins/observability/public/hooks/slo/active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/active_alerts.ts new file mode 100644 index 0000000000000..2a4b387df32a0 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/active_alerts.ts @@ -0,0 +1,39 @@ +/* + * 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 { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; + +type SLO = Pick; +export class ActiveAlerts { + private data: Map = new Map(); + + constructor(initialData?: Record) { + if (initialData) { + Object.keys(initialData).forEach((key) => this.data.set(key, initialData[key])); + } + } + + set(slo: SLO, value: number) { + this.data.set(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`, value); + } + + get(slo: SLO) { + return this.data.get(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } + + has(slo: SLO) { + return this.data.has(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } + + delete(slo: SLO) { + return this.data.delete(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } + + clear() { + return this.data.clear(); + } +} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts index 33ae5ffac2637..2f91147d9bbf6 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts @@ -8,43 +8,12 @@ import { useQuery } from '@tanstack/react-query'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; -import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; import { AlertConsumers } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; -import { SLO_LONG_REFETCH_INTERVAL } from '../../constants'; - -type SLO = Pick; - -export class ActiveAlerts { - private data: Map = new Map(); - - constructor(initialData?: Record) { - if (initialData) { - Object.keys(initialData).forEach((key) => this.data.set(key, initialData[key])); - } - } - - set(slo: SLO, value: number) { - this.data.set(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`, value); - } +import { ActiveAlerts } from './active_alerts'; - get(slo: SLO) { - return this.data.get(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); - } - - has(slo: SLO) { - return this.data.has(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); - } - - delete(slo: SLO) { - return this.data.delete(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); - } - - clear() { - return this.data.clear(); - } -} +import { SLO_LONG_REFETCH_INTERVAL } from '../../constants'; type SloIdAndInstanceId = [string, string]; diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts index a05ec3c616950..f3f51de9d4890 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts @@ -20,6 +20,7 @@ interface SLOListParams { sortBy?: string; sortDirection?: 'asc' | 'desc'; shouldRefetch?: boolean; + perPage?: number; } export interface UseFetchSloListResponse { @@ -37,6 +38,7 @@ export function useFetchSloList({ sortBy = 'status', sortDirection = 'desc', shouldRefetch, + perPage, }: SLOListParams = {}): UseFetchSloListResponse { const { http, @@ -56,6 +58,7 @@ export function useFetchSloList({ ...(sortBy && { sortBy }), ...(sortDirection && { sortDirection }), ...(page && { page }), + ...(perPage && { perPage }), }, signal, }); diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index 92ea62f385ab8..de21fb145677f 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -15,7 +15,8 @@ import { useLicense } from '../../hooks/use_license'; import { useCapabilities } from '../../hooks/slo/use_capabilities'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; -import { ActiveAlerts, useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; +import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; +import { ActiveAlerts } from '../../hooks/slo/active_alerts'; import { useCloneSlo } from '../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { render } from '../../utils/test_helper'; diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx index 5feec4691cd0a..bcb3a1a0c5111 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx @@ -14,7 +14,7 @@ import { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo'; export interface Props { rules: Array> | undefined; - onClick: () => void; + onClick?: () => void; } export function SloRulesBadge({ rules, onClick }: Props) { diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx index ac649a71e3600..8da351e5ba0e3 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx @@ -41,7 +41,7 @@ export interface Props { cardsPerRow: number; } -const useCardColor = (status?: SLOWithSummaryResponse['summary']['status']) => { +export const useSloCardColor = (status?: SLOWithSummaryResponse['summary']['status']) => { const colors = { DEGRADING: useEuiBackgroundColor('warning'), VIOLATED: useEuiBackgroundColor('danger'), @@ -57,10 +57,6 @@ const getSubTitle = (slo: SLOWithSummaryResponse, cardsPerRow: number) => { }; export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cardsPerRow }: Props) { - const { - application: { navigateToUrl }, - } = useKibana().services; - const containerRef = React.useRef(null); const [isMouseOver, setIsMouseOver] = useState(false); @@ -68,12 +64,6 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); - const { sliValue, sloTarget, sloDetailsUrl } = useSloFormattedSummary(slo); - - const cardColor = useCardColor(slo.summary.status); - - const subTitle = getSubTitle(slo, cardsPerRow); - const historicalSliData = formatHistoricalData(historicalSummary, 'sli_value'); const { handleCreateRule, handleDeleteCancel, handleDeleteConfirm } = useSloListActions({ @@ -105,45 +95,7 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards }} title={slo.summary.status} > - - { - if (isMetricElementEvent(d)) { - navigateToUrl(sloDetailsUrl); - } - }} - locale={i18n.getLocale()} - /> - ({ - x: d.key as number, - y: d.value as number, - })), - extra: ( - - ), - icon: () => , - color: cardColor, - }, - ], - ]} - /> - + {(isMouseOver || isActionsPopoverOpen) && ( ); } + +export function SloCardChart({ + slo, + cardsPerRow, + historicalSliData, +}: { + slo: SLOWithSummaryResponse; + cardsPerRow: number; + historicalSliData?: Array<{ key?: number; value?: number }>; +}) { + const { + application: { navigateToUrl }, + } = useKibana().services; + + const cardColor = useSloCardColor(slo.summary.status); + const subTitle = getSubTitle(slo, cardsPerRow); + const { sliValue, sloTarget, sloDetailsUrl } = useSloFormattedSummary(slo); + + return ( + + { + if (isMetricElementEvent(d)) { + navigateToUrl(sloDetailsUrl); + } + }} + locale={i18n.getLocale()} + /> + ({ + x: d.key as number, + y: d.value as number, + })), + extra: ( + + ), + icon: () => , + color: cardColor, + }, + ], + ]} + /> + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_badges.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_badges.tsx index 4a3663b0a8c54..1fec95eb98f73 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_badges.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_badges.tsx @@ -22,7 +22,7 @@ interface Props { activeAlerts?: number; slo: SLOWithSummaryResponse; rules: Array> | undefined; - handleCreateRule: () => void; + handleCreateRule?: () => void; } const Container = styled.div` diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx index 4e4df93967fde..a8d43c35217d1 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { SLOWithSummaryResponse, ALL_VALUE } from '@kbn/slo-schema'; import { EuiFlexGridProps } from '@elastic/eui/src/components/flex/flex_grid'; -import { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; +import { ActiveAlerts } from '../../../../hooks/slo/active_alerts'; import type { UseFetchRulesForSloResponse } from '../../../../hooks/slo/use_fetch_rules_for_slo'; import { useFetchHistoricalSummary } from '../../../../hooks/slo/use_fetch_historical_summary'; import { SloCardItem } from './slo_card_item'; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx index aa8c4f590b9e6..1c6cdfe51700c 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; -import { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; +import { ActiveAlerts } from '../../../hooks/slo/active_alerts'; import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; import { SloListItems as Component, Props } from './slo_list_items'; import { sloList } from '../../../data/slo/slo'; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx index 8e31995d1290b..75fa9632d15bd 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; -import { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; +import { ActiveAlerts } from '../../../hooks/slo/active_alerts'; import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary'; import { UseFetchRulesForSloResponse } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { SloListItem } from './slo_list_item'; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index e52e6c353c2e7..12848e1bb4974 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -8,6 +8,7 @@ import { CasesDeepLinkId, CasesUiStart, getCasesDeepLinks } from '@kbn/cases-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; +import type { IUiSettingsClient } from '@kbn/core/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { AppDeepLink, @@ -34,6 +35,8 @@ import type { ObservabilitySharedPluginSetup, ObservabilitySharedPluginStart, } from '@kbn/observability-shared-plugin/public'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; + import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import { TriggersAndActionsUIPublicPluginSetup, @@ -60,8 +63,9 @@ import { import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; -import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; +import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import { firstValueFrom } from 'rxjs'; + import { observabilityAppId, observabilityFeatureId } from '../common'; import { ALERTS_PATH, @@ -115,6 +119,7 @@ export interface ObservabilityPublicPluginsSetup { home?: HomePublicPluginSetup; usageCollection: UsageCollectionSetup; embeddable: EmbeddableSetup; + uiActions: UiActionsSetup; licensing: LicensingPluginSetup; } export interface ObservabilityPublicPluginsStart { @@ -144,6 +149,8 @@ export interface ObservabilityPublicPluginsStart { cloud?: CloudStart; aiops: AiopsPluginStart; serverless?: ServerlessPluginStart; + uiSettings: IUiSettingsClient; + uiActions: UiActionsStart; } export type ObservabilityPublicStart = ReturnType; @@ -309,6 +316,25 @@ export class Plugin pluginsSetup.embeddable.registerEmbeddableFactory(factory.type, factory); }; registerSloOverviewEmbeddableFactory(); + const registerSloAlertsEmbeddableFactory = async () => { + const { SloAlertsEmbeddableFactoryDefinition } = await import( + './embeddable/slo/alerts/slo_alerts_embeddable_factory' + ); + const factory = new SloAlertsEmbeddableFactoryDefinition( + coreSetup.getStartServices, + kibanaVersion + ); + pluginsSetup.embeddable.registerEmbeddableFactory(factory.type, factory); + }; + registerSloAlertsEmbeddableFactory(); + + const registerAsyncSloAlertsUiActions = async () => { + if (pluginsSetup.uiActions) { + const { registerSloAlertsUiActions } = await import('./ui_actions'); + registerSloAlertsUiActions(pluginsSetup.uiActions, coreSetup); + } + }; + registerAsyncSloAlertsUiActions(); } }; @@ -417,6 +443,17 @@ export class Plugin alertsTableConfigurationRegistry.register(alertsTableConfig); }); + const getAsyncSloEmbeddableAlertsTableConfiguration = async () => { + const { getSloAlertsTableConfiguration } = await import( + './components/alerts_table/slo/get_slo_alerts_table_configuration' + ); + return getSloAlertsTableConfiguration(this.observabilityRuleTypeRegistry, config); + }; + + getAsyncSloEmbeddableAlertsTableConfiguration().then((alertsTableConfig) => { + alertsTableConfigurationRegistry.register(alertsTableConfig); + }); + return { observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry, useRulesLink: createUseRulesLink(), diff --git a/x-pack/plugins/observability/public/ui_actions/edit_slo_alerts_panel.tsx b/x-pack/plugins/observability/public/ui_actions/edit_slo_alerts_panel.tsx new file mode 100644 index 0000000000000..b920c47caa352 --- /dev/null +++ b/x-pack/plugins/observability/public/ui_actions/edit_slo_alerts_panel.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { ViewMode } from '@kbn/embeddable-plugin/common'; +import type { CoreSetup } from '@kbn/core/public'; +import { IEmbeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public'; +import { SLO_ALERTS_EMBEDDABLE } from '../embeddable/slo/constants'; +import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '..'; +import { SloAlertsEmbeddableInput } from '../embeddable/slo/alerts/types'; + +export const EDIT_SLO_ALERTS_ACTION = 'editSloAlertsPanelAction'; +export interface EditSloAlertsPanelContext { + embeddable: IEmbeddable; +} +export function createEditSloAlertsPanelAction( + getStartServices: CoreSetup< + ObservabilityPublicPluginsStart, + ObservabilityPublicStart + >['getStartServices'] +): UiActionsActionDefinition { + return { + id: EDIT_SLO_ALERTS_ACTION, + type: EDIT_SLO_ALERTS_ACTION, + getIconType(context): string { + return 'pencil'; + }, + getDisplayName: () => + i18n.translate('xpack.observability.actions.editSloAlertsEmbeddableTitle', { + defaultMessage: 'Edit configuration', + }), + async execute({ embeddable }) { + if (!embeddable) { + throw new Error('Not possible to execute an action without the embeddable context'); + } + + const [coreStart, pluginStart] = await getStartServices(); + + try { + const { resolveEmbeddableSloUserInput } = await import( + '../embeddable/slo/alerts/handle_explicit_input' + ); + + const result = await resolveEmbeddableSloUserInput( + coreStart, + pluginStart, + embeddable.getInput() + ); + embeddable.updateInput(result); + } catch (e) { + return Promise.reject(); + } + }, + async isCompatible({ embeddable }) { + return ( + embeddable.type === SLO_ALERTS_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT + ); + }, + }; +} diff --git a/x-pack/plugins/observability/public/ui_actions/index.ts b/x-pack/plugins/observability/public/ui_actions/index.ts new file mode 100644 index 0000000000000..59929b54f597f --- /dev/null +++ b/x-pack/plugins/observability/public/ui_actions/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; +import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; +import type { CoreSetup } from '@kbn/core/public'; +import { createEditSloAlertsPanelAction } from './edit_slo_alerts_panel'; +import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '..'; + +export function registerSloAlertsUiActions( + uiActions: UiActionsSetup, + core: CoreSetup +) { + // Initialize actions + const editSloAlertsPanelAction = createEditSloAlertsPanelAction(core.getStartServices); + // Register actions + uiActions.registerAction(editSloAlertsPanelAction); + // Assign and register triggers + uiActions.attachAction(CONTEXT_MENU_TRIGGER, editSloAlertsPanelAction.id); +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index eb29d6aec21dd..2e784f1fb88f8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29568,7 +29568,6 @@ "xpack.observability.sloEmbeddable.displayName": "Aperçu du SLO", "xpack.observability.sloEmbeddable.displayTitle": "Aperçu du SLO", "xpack.observability.sloEmbeddable.overview.sloNotFoundText": "Le SLO a été supprimé. Vous pouvez supprimer sans risque le widget du tableau de bord.", - "xpack.observability.sloEmbeddable.overview.sloTargetLabel": "Cible", "xpack.observability.slos.sloDetails.headerControl.exploreInApm": "Détails du service", "xpack.observability.slosLinkTitle": "SLO", "xpack.observability.slosPage.autoRefreshButtonLabel": "Actualisation automatique", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bf155e5d63898..93d3318cc7700 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29568,7 +29568,6 @@ "xpack.observability.sloEmbeddable.displayName": "SLO概要", "xpack.observability.sloEmbeddable.displayTitle": "SLO概要", "xpack.observability.sloEmbeddable.overview.sloNotFoundText": "SLOが削除されました。ウィジェットをダッシュボードから安全に削除できます。", - "xpack.observability.sloEmbeddable.overview.sloTargetLabel": "目標", "xpack.observability.slos.sloDetails.headerControl.exploreInApm": "サービス詳細", "xpack.observability.slosLinkTitle": "SLO", "xpack.observability.slosPage.autoRefreshButtonLabel": "自動更新", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 882f1042fa930..58f58128acb9b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29565,7 +29565,6 @@ "xpack.observability.sloEmbeddable.displayName": "SLO 概览", "xpack.observability.sloEmbeddable.displayTitle": "SLO 概览", "xpack.observability.sloEmbeddable.overview.sloNotFoundText": "SLO 已删除。您可以放心从仪表板中删除小组件。", - "xpack.observability.sloEmbeddable.overview.sloTargetLabel": "目标", "xpack.observability.slos.sloDetails.headerControl.exploreInApm": "服务详情", "xpack.observability.slosLinkTitle": "SLO", "xpack.observability.slosPage.autoRefreshButtonLabel": "自动刷新", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx index b880d93d63179..2031c9a1f3fe2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useLoadAlertSummary } from '../../hooks/use_load_alert_summary'; import { AlertSummaryWidgetProps } from '.'; import { @@ -23,6 +23,7 @@ export const AlertSummaryWidget = ({ onClick = () => {}, timeRange, hideChart, + onLoaded, }: AlertSummaryWidgetProps) => { const { alertSummary: { activeAlertCount, activeAlerts, recoveredAlertCount }, @@ -34,6 +35,12 @@ export const AlertSummaryWidget = ({ timeRange, }); + useEffect(() => { + if (!isLoading && onLoaded) { + onLoaded(); + } + }, [isLoading, onLoaded]); + if (isLoading) return ; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts index 7e7e7355e1a2a..019b6018e69b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts @@ -38,4 +38,5 @@ export interface AlertSummaryWidgetProps { timeRange: AlertSummaryTimeRange; chartProps: ChartProps; hideChart?: boolean; + onLoaded?: () => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index 28fc689d94fc6..7103dfc8bea6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -66,6 +66,7 @@ export type AlertsTableStateProps = { pageSize?: number; browserFields?: BrowserFields; onUpdate?: (args: TableUpdateHandlerArgs) => void; + onLoaded?: () => void; runtimeMappings?: MappingRuntimeFields; showAlertStatusWithFlapping?: boolean; toolbarVisibility?: EuiDataGridToolBarVisibilityOptions; @@ -77,6 +78,7 @@ export type AlertsTableStateProps = { * Enable when rows may have variable heights (disables virtualization) */ dynamicRowHeight?: boolean; + lastReloadRequestTime?: number; } & Partial; export interface AlertsTableStorage { @@ -146,11 +148,13 @@ const AlertsTableStateWithQueryProvider = ({ gridStyle, browserFields: propBrowserFields, onUpdate, + onLoaded, runtimeMappings, showAlertStatusWithFlapping, toolbarVisibility, shouldHighlightRow, dynamicRowHeight, + lastReloadRequestTime, }: AlertsTableStateProps) => { const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; const hasAlertsTableConfiguration = @@ -245,6 +249,7 @@ const AlertsTableStateWithQueryProvider = ({ query, pagination, onPageChange, + onLoaded, runtimeMappings, sort, skip: false, @@ -267,6 +272,11 @@ const AlertsTableStateWithQueryProvider = ({ onUpdate({ isLoading, totalCount: alertsCount, refresh }); } }, [isLoading, alertsCount, onUpdate, refresh]); + useEffect(() => { + if (lastReloadRequestTime) { + refresh(); + } + }, [lastReloadRequestTime, refresh]); const caseIds = useMemo(() => getCaseIdsFromAlerts(alerts), [alerts]); const maintenanceWindowIds = useMemo(() => getMaintenanceWindowIdsFromAlerts(alerts), [alerts]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx index acf00da71b5f7..9b62174661e62 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx @@ -36,6 +36,7 @@ export interface FetchAlertsArgs { pageIndex: number; pageSize: number; }; + onLoaded?: () => void; onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void; runtimeMappings?: MappingRuntimeFields; sort: SortCombinations[]; @@ -147,6 +148,7 @@ export type UseFetchAlerts = ({ fields, query, pagination, + onLoaded, onPageChange, runtimeMappings, skip, @@ -157,6 +159,7 @@ const useFetchAlerts = ({ fields, query, pagination, + onLoaded, onPageChange, runtimeMappings, skip, @@ -258,11 +261,14 @@ const useFetchAlerts = ({ ecsAlertsData, totalAlerts, }); + dispatch({ type: 'loading', loading: false }); + onLoaded?.(); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { dispatch({ type: 'loading', loading: false }); + onLoaded?.(); data.search.showError(msg); searchSubscription$.current.unsubscribe(); }, @@ -275,7 +281,7 @@ const useFetchAlerts = ({ asyncSearch(); refetch.current = asyncSearch; }, - [skip, data, featureIds, query, fields] + [skip, data, featureIds, query, fields, onLoaded] ); // FUTURE ENGINEER