diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts b/x-pack/plugins/stack_alerts/common/comparator_types.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts rename to x-pack/plugins/stack_alerts/common/comparator_types.ts diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 898c080185ee6d..871f156afd14f3 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export * from './config'; +export * from './render_rule_params'; +export { ComparatorFns, ComparatorFnNames, getHumanReadableComparator } from './comparator_types'; export const STACK_ALERTS_FEATURE_ID = 'stackAlerts'; diff --git a/x-pack/plugins/stack_alerts/common/render_rule_params.ts b/x-pack/plugins/stack_alerts/common/render_rule_params.ts new file mode 100644 index 00000000000000..bdd65ddaf8b1cd --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/render_rule_params.ts @@ -0,0 +1,18 @@ +/* + * 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 { getHumanReadableComparator } from './comparator_types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function renderIndexThresholdParams(params: any) { + const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`; + const conditions = `${agg} is ${getHumanReadableComparator( + params.thresholdComparator + )} ${params.threshold.join(' and ')}`; + const window = `${params.timeWindowSize}${params.timeWindowUnit}`; + return `${conditions} over ${window}`; +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts index 1b229bb4a9d0a1..9879bcd0b21e2f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { validateExpression } from './validation'; import { IndexThresholdAlertParams } from './types'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { renderIndexThresholdParams } from '../../../common'; export function getAlertType(): AlertTypeModel { return { @@ -21,6 +22,7 @@ export function getAlertType(): AlertTypeModel { documentationUrl: (docLinks) => docLinks.links.alerting.indexThreshold, alertParamsExpression: lazy(() => import('./expression')), validate: validateExpression, + formatter: renderIndexThresholdParams, defaultActionMessage: i18n.translate( 'xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index 58d9c725cfd1bb..34d587dc9d8c3e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { ComparatorFnNames } from '../lib'; +import { ComparatorFnNames } from '../../../common'; import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; import { AlertTypeState } from '../../../../alerting/server'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts index 871b71e50b364d..83f23833fc5b99 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts @@ -9,8 +9,11 @@ import { i18n } from '@kbn/i18n'; import type { estypes } from '@elastic/elasticsearch'; import { RegisterAlertTypesParams } from '..'; import { createThresholdRuleType } from '../../types'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; -import { ComparatorFns, getHumanReadableComparator } from '../lib'; +import { + STACK_ALERTS_FEATURE_ID, + ComparatorFns, + getHumanReadableComparator, +} from '../../../common'; import * as EsQuery from './alert_type'; import { getSearchParams, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts index 957d33ea730c77..8074bd72c088e3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Params } from './alert_type_params'; import { AlertInstanceContext } from '../../../../alerting/server'; +import { renderIndexThresholdParams } from '../../../common'; // alert type context provided to actions @@ -26,8 +27,6 @@ export interface BaseActionContext extends AlertInstanceContext { date: string; // the value that met the threshold value: number; - // threshold conditions - conditions: string; } export function addMessages( @@ -43,21 +42,19 @@ export function addMessages( }, }); - const window = `${params.timeWindowSize}${params.timeWindowUnit}`; const message = i18n.translate( 'xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription', { defaultMessage: `alert '{name}' is active for group '{group}': - Value: {value} -- Conditions Met: {conditions} over {window} +- Conditions Met: {conditionsMet} - Timestamp: {date}`, values: { name: alertName, group: baseContext.group, value: baseContext.value, - conditions: baseContext.conditions, - window, + conditionsMet: renderIndexThresholdParams(params), date: baseContext.date, }, } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index d32e7890b17c60..c4c22c581aba96 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { ComparatorFnNames } from '../lib'; +import { ComparatorFnNames } from '../../../common'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts index c86f6fde9e90a9..ad8474649255ba 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts @@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n'; import { RegisterAlertTypesParams } from '..'; import { createThresholdRuleType } from '../../types'; import * as IndexThreshold from './alert_type'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { STACK_ALERTS_FEATURE_ID, ComparatorFns } from '../../../common'; import { ParamsSchema } from './alert_type_params'; -import { ComparatorFns, getHumanReadableComparator } from '../lib'; import { TimeSeriesQuery } from '../../../../triggers_actions_ui/server'; import { BaseActionContext, addMessages } from './action_context'; @@ -121,18 +120,10 @@ export function register(registerParams: RegisterAlertTypesParams) { if (!met) continue; - const agg = params.aggField - ? `${params.aggType}(${params.aggField})` - : `${params.aggType}`; - const humanFn = `${agg} is ${getHumanReadableComparator( - params.thresholdComparator - )} ${params.threshold.join(' and ')}`; - const baseContext: BaseActionContext = { date, group: instanceId, value, - conditions: humanFn, }; const actionContext = addMessages(rule.name, baseContext, params); writeRuleAlert({ diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts deleted file mode 100644 index 09219aad6fe5e9..00000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { ComparatorFns, ComparatorFnNames, getHumanReadableComparator } from './comparator_types'; diff --git a/x-pack/plugins/stack_alerts/common/config.ts b/x-pack/plugins/stack_alerts/server/config.ts similarity index 100% rename from x-pack/plugins/stack_alerts/common/config.ts rename to x-pack/plugins/stack_alerts/server/config.ts diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index bd10a486fa531b..3c56a4caaadd66 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -7,7 +7,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; -import { configSchema, Config } from '../common/config'; +import { configSchema, Config } from './config'; export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type'; export const config: PluginConfigDescriptor = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 79100a585a973f..9fbe1db7a762fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -35,8 +35,6 @@ const TriggersActionsUIHome = lazy(async () => import('./home')); const AlertDetailsRoute = lazy( () => import('./sections/alert_details/components/alert_details_route') ); -const AlertDataRoute = lazy(() => import('./sections/alert_details/components/alert_data_route')); - export interface TriggersAndActionsUiServices extends CoreStart { data: DataPublicPluginStart; charts: ChartsPluginStart; @@ -94,7 +92,7 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = { + return await http.get(`/internal/stack_alerts/rule/${alertId}/_alert_data`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts index a0b090a474e283..1285f1f28faf3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts @@ -14,6 +14,7 @@ export { disableAlert, disableAlerts } from './disable'; export { enableAlert, enableAlerts } from './enable'; export { loadAlert } from './get_rule'; export { loadAlertInstanceSummary } from './alert_summary'; +export { AlertData, loadAlertData } from './alert_data'; export { muteAlertInstance } from './mute_alert'; export { muteAlert, muteAlerts } from './mute'; export { loadAlertTypes } from './rule_types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_data_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_data_route.tsx deleted file mode 100644 index 135a8e62b21baf..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_data_route.tsx +++ /dev/null @@ -1,139 +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 { EuiCallOut } from '@elastic/eui'; -import React, { useState, useEffect } from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import { ToastsApi } from 'kibana/public'; -import { Alert, AlertType, ActionType } from '../../../../types'; -import { AlertDetailsWithApi as AlertDetails } from './alert_details'; -import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; -import { - ComponentOpts as AlertApis, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; -import { - ComponentOpts as ActionApis, - withActionOperations, -} from '../../common/components/with_actions_api_operations'; -import { useKibana } from '../../../../common/lib/kibana'; -import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; - -type AlertDataRouteProps = RouteComponentProps<{ - ruleId: string; -}> & - Pick & - Pick; - -export const AlertDataRoute: React.FunctionComponent = ({ - match: { - params: { ruleId }, - }, - loadAlert, - loadAlertTypes, - loadActionTypes, -}) => { - const { - http, - notifications: { toasts }, - } = useKibana().services; - - const [alert, setAlert] = useState(null); - const [alertType, setAlertType] = useState(null); - const [actionTypes, setActionTypes] = useState(null); - const [refreshToken, requestRefresh] = React.useState(); - useEffect(() => { - getAlertData( - ruleId, - loadAlert, - loadAlertTypes, - loadActionTypes, - setAlert, - setAlertType, - setActionTypes, - toasts - ); - }, [ruleId, http, loadActionTypes, loadAlert, loadAlertTypes, toasts, refreshToken]); - - return ( - <> - -

- {i18n.translate('xpack.observability.alertsDisclaimerText', { - defaultMessage: - 'This page shows an experimental alerting view. The data shown here will probably not be an accurate representation of alerts.', - })} -

-
- {alert && alertType && actionTypes ? ( - requestRefresh(Date.now())} - /> - ) : ( - - )} - - ); -}; - -export async function getAlertData( - alertId: string, - loadAlert: AlertApis['loadAlert'], - loadAlertTypes: AlertApis['loadAlertTypes'], - loadActionTypes: ActionApis['loadActionTypes'], - setAlert: React.Dispatch>, - setAlertType: React.Dispatch>, - setActionTypes: React.Dispatch>, - toasts: Pick -) { - try { - const loadedAlert = await loadAlert(alertId); - setAlert(loadedAlert); - - const [loadedAlertType, loadedActionTypes] = await Promise.all([ - loadAlertTypes() - .then((types) => types.find((type) => type.id === loadedAlert.alertTypeId)) - .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), - loadActionTypes().then( - throwIfIsntContained( - new Set(loadedAlert.actions.map((action) => action.actionTypeId)), - (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, - (action: ActionType) => action.id - ) - ), - ]); - - setAlertType(loadedAlertType); - setActionTypes(loadedActionTypes); - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', - { - defaultMessage: 'Unable to load rule: {message}', - values: { - message: e.message, - }, - } - ), - }); - } -} - -const AlertDataRouteWithApi = withActionOperations(withBulkAlertOperations(AlertDataRoute)); -// eslint-disable-next-line import/no-default-export -export { AlertDataRouteWithApi as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0796f09b134606..9b3be27856671b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -38,9 +38,10 @@ import { withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesRouteWithApi } from './alert_instances_route'; +import { AlertDataRouteWithApi } from './experimental/alert_data_route'; import { ViewInApp } from './view_in_app'; import { AlertEdit } from '../../alert_form'; -import { routeToRuleDetails } from '../../../constants'; +import { routeToRuleDetails, routeToAlertData } from '../../../constants'; import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations'; import { useKibana } from '../../../../common/lib/kibana'; import { alertReducer } from '../../alert_form/alert_reducer'; @@ -48,6 +49,7 @@ import { alertReducer } from '../../alert_form/alert_reducer'; type AlertDetailsProps = { alert: Alert; alertType: AlertType; + showExperimentalAlertsAsData: boolean; actionTypes: ActionType[]; requestRefresh: () => Promise; } & Pick; @@ -61,6 +63,7 @@ export const AlertDetails: React.FunctionComponent = ({ unmuteAlert, muteAlert, requestRefresh, + showExperimentalAlertsAsData, }) => { const history = useHistory(); const { @@ -109,7 +112,8 @@ export const AlertDetails: React.FunctionComponent = ({ const [dissmissAlertErrors, setDissmissAlertErrors] = useState(false); const setAlert = async () => { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + const routeToUse = showExperimentalAlertsAsData ? routeToAlertData : routeToRuleDetails; + history.push(routeToUse.replace(`:ruleId`, alert.id)); }; const getAlertStatusErrorReasonText = () => { @@ -186,6 +190,32 @@ export const AlertDetails: React.FunctionComponent = ({ + {showExperimentalAlertsAsData && ( + + + +

+ {i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.experimentalDisclaimerText', + { + defaultMessage: + 'This page shows an experimental alerting view. The data shown here will probably not be an accurate representation of alerts.', + } + )} +

+
+
+
+ )} @@ -323,12 +353,21 @@ export const AlertDetails: React.FunctionComponent = ({ {alert.enabled ? ( - + showExperimentalAlertsAsData ? ( + + ) : ( + + ) ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx index 2d6db5f6330ccc..2c4556075067c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx @@ -32,6 +32,7 @@ type AlertDetailsRouteProps = RouteComponentProps<{ export const AlertDetailsRoute: React.FunctionComponent = ({ match: { params: { ruleId }, + path, }, loadAlert, loadAlertTypes, @@ -42,6 +43,7 @@ export const AlertDetailsRoute: React.FunctionComponent notifications: { toasts }, } = useKibana().services; + const [showExperimentalAlertsAsData] = useState(path.includes('/alerts')); const [alert, setAlert] = useState(null); const [alertType, setAlertType] = useState(null); const [actionTypes, setActionTypes] = useState(null); @@ -63,6 +65,7 @@ export const AlertDetailsRoute: React.FunctionComponent requestRefresh(Date.now())} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data.tsx new file mode 100644 index 00000000000000..718ad80cff686e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data.tsx @@ -0,0 +1,357 @@ +/* + * 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, { Fragment, useEffect, useState } from 'react'; +import moment, { Duration } from 'moment'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiHealth, + EuiIconTip, + EuiSpacer, + EuiSwitch, + EuiToolTip, +} from '@elastic/eui'; +// @ts-ignore +import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; +import { padStart, chunk } from 'lodash'; +import { ActionGroup, AlertInstanceStatusValues } from '../../../../../../../alerting/common'; +import { Alert, AlertInstanceStatus, AlertType, Pagination } from '../../../../../types'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../../common/components/with_bulk_alert_api_operations'; +import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../../constants'; +import { AlertData } from '../../../../lib/alert_api'; +import { useKibana } from '../../../../../common/lib/kibana'; + +interface AlertTableData extends AlertData { + active: boolean; + start: number; +} + +type AlertDataTableProps = { + alert: Alert; + alertType: AlertType; + readOnly: boolean; + alertData: AlertData[]; + requestRefresh: () => Promise; + durationEpoch?: number; +} & Pick; + +function durationAsString(duration: Duration): string { + return [duration.hours(), duration.minutes(), duration.seconds()] + .map((value) => padStart(`${value}`, 2, '0')) + .join(':'); +} + +const columns = (alert, alertTypeModel) => [ + { + field: 'active', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.statusColumnDescription', + { + defaultMessage: 'Status', + } + ), + align: 'center', + render: (active: boolean) => { + return active ? ( + + ) : ( + + ); + }, + }, + { + field: 'id', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.alertIdColumnDescription', + { + defaultMessage: 'Alert ID', + } + ), + }, + { + field: 'start', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.triggeredColumnDescription', + { + defaultMessage: 'Triggered', + } + ), + render: (start: number) => { + const time = new Date(start).getTime(); + const momentTime = moment(time); + const relativeTimeLabel = momentTime.fromNow(); + const absoluteTimeLabel = momentTime.format(`MMM D, YYYY, HH:mm:ss.SSS Z`); + return ( + + <>{relativeTimeLabel} + + ); + }, + }, + { + field: 'duration', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.durationColumnDescription', + { + defaultMessage: 'Duration', + } + ), + render: (duration: number, item: AlertTableData) => { + const { active } = item; + return active || duration == null ? null : durationAsString(moment.duration(duration / 1000)); + }, + }, + { + field: 'reason', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.reasonColumnDescription', + { + defaultMessage: 'Reason', + } + ), + dataType: 'string', + render: () => { + return alertTypeModel && alertTypeModel.formatter + ? alertTypeModel.formatter(alert.params) + : 'Threshold condition met'; + }, + }, + // { + // field: '', + // align: RIGHT_ALIGNMENT, + // width: '60px', + // name: i18n.translate( + // 'xpack.triggersActionsUI.sections.alertDetails.alertsTable.muteActionDescription', + // { defaultMessage: 'Mute' } + // ), + // render: (_, alert: AlertTableData) => { + // return ( + // + // onMuteAction(alertInstance)} + // /> + // + // ); + // }, + // sortable: false, + // 'data-test-subj': 'alertInstancesTableCell-actions', + // }, +]; + +export function AlertDataTable({ + alert, + alertType, + readOnly, + alertData, + muteAlertInstance, + unmuteAlertInstance, + requestRefresh, + durationEpoch = Date.now(), +}: AlertDataTableProps) { + console.log(alertData); + const [pagination, setPagination] = useState({ + index: 0, + size: DEFAULT_SEARCH_PAGE_SIZE, + }); + const [alertTableData, setAlertTableData] = useState([]); + + const { alertTypeRegistry } = useKibana().services; + const alertTypeModel = alertTypeRegistry.get(alert.alertTypeId); + useEffect(() => { + if (alertData && alertData.length > 0) { + setAlertTableData( + alertData.map((data: AlertData) => { + // const ruleType = observabilityRuleRegistry.getTypeByRuleId(alert['rule.id']); + // const formatted = { + // reason: alert['rule.name'], + // ...(ruleType?.format?.({ alert, formatters: { asDuration, asPercent } }) ?? {}), + // }; + + // const parsedLink = formatted.link ? parse(formatted.link, true) : undefined; + + return { + ...data, + // ...formatted, + // link: parsedLink + // ? format({ + // ...parsedLink, + // query: { + // ...parsedLink.query, + // rangeFrom, + // rangeTo, + // }, + // }) + // : undefined, + active: data['event.action'] !== 'recovered', + start: new Date(data['kibana.rac.alert.start']).getTime(), + id: data['kibana.rac.alert.id'], + duration: data['kibana.rac.alert.duration.us'], + }; + }) + ); + } else { + setAlertTableData([]); + } + }, [alertData]); + + // const alertInstances = Object.entries(alertInstanceSummary.instances) + // .map(([instanceId, instance]) => + // alertInstanceToListItem(durationEpoch, alertType, instanceId, instance) + // ) + // .sort((leftInstance, rightInstance) => leftInstance.sortPriority - rightInstance.sortPriority); + + // const pageOfAlertInstances = getPage(alertInstances, pagination); + + // const onMuteAction = async (instance: AlertInstanceListItem) => { + // await (instance.isMuted + // ? unmuteAlertInstance(alert, instance.instance) + // : muteAlertInstance(alert, instance.instance)); + // requestRefresh(); + // }; + + return ( + + + + {/* + { + setPagination(changedPage); + }} + rowProps={() => ({ + 'data-test-subj': 'alert-instance-row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + columns={alertInstancesTableColumns(onMuteAction, readOnly)} + data-test-subj="alertInstancesList" + tableLayout="fixed" + className="alertInstancesList" + /> */} + + ); +} +export const AlertDataTableWithApi = withBulkAlertOperations(AlertDataTable); + +function getPage(items: any[], pagination: Pagination) { + return chunk(items, pagination.size)[pagination.index] || []; +} + +interface AlertInstanceListItemStatus { + label: string; + healthColor: string; + actionGroup?: string; +} +export interface AlertInstanceListItem { + instance: string; + status: AlertInstanceListItemStatus; + start?: Date; + duration: number; + isMuted: boolean; + sortPriority: number; +} + +const ACTIVE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active', + { defaultMessage: 'Active' } +); + +const INACTIVE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', + { defaultMessage: 'OK' } +); + +function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { + actionGroupId = actionGroupId || alertType.defaultActionGroupId; + const actionGroup = alertType?.actionGroups?.find( + (group: ActionGroup) => group.id === actionGroupId + ); + return actionGroup?.name; +} + +export function alertInstanceToListItem( + durationEpoch: number, + alertType: AlertType, + instanceId: string, + instance: AlertInstanceStatus +): AlertInstanceListItem { + const isMuted = !!instance?.muted; + const status = + instance?.status === 'Active' + ? { + label: ACTIVE_LABEL, + actionGroup: getActionGroupName(alertType, instance?.actionGroupId), + healthColor: 'primary', + } + : { label: INACTIVE_LABEL, healthColor: 'subdued' }; + const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; + const duration = start ? durationEpoch - start.valueOf() : 0; + const sortPriority = getSortPriorityByStatus(instance?.status); + return { + instance: instanceId, + status, + start, + duration, + isMuted, + sortPriority, + }; +} + +function getSortPriorityByStatus(status?: AlertInstanceStatusValues): number { + switch (status) { + case 'Active': + return 0; + case 'OK': + return 1; + } + return 2; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data_route.tsx new file mode 100644 index 00000000000000..37ff6f6829eac3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/experimental/alert_data_route.tsx @@ -0,0 +1,82 @@ +/* + * 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 { ToastsApi } from 'kibana/public'; +import React, { useState, useEffect } from 'react'; +import { Alert, AlertType } from '../../../../../types'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../../common/components/with_bulk_alert_api_operations'; +import { AlertDataTableWithApi } from './alert_data'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { CenterJustifiedSpinner } from '../../../../components/center_justified_spinner'; +import { AlertData } from '../../../../lib/alert_api'; + +type WithAlertDataProps = { + alert: Alert; + alertType: AlertType; + readOnly: boolean; + requestRefresh: () => Promise; +} & Pick; + +export const AlertDataRoute: React.FunctionComponent = ({ + alert, + alertType, + readOnly, + requestRefresh, + loadAlertData, +}) => { + const { + notifications: { toasts }, + } = useKibana().services; + + const [alertData, setAlertData] = useState(null); + + useEffect(() => { + getAlertData(alert.id, loadAlertData, setAlertData, toasts); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alert]); + + return alertData ? ( + + ) : ( + + ); +}; + +export async function getAlertData( + alertId: string, + loadAlertData: AlertApis['loadAlertData'], + setAlertData: React.Dispatch>, + toasts: Pick +) { + try { + setAlertData(await loadAlertData(alertId)); + } catch (e) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.experimentalAlertData.unableToLoadAlertDataMessage', + { + defaultMessage: 'Unable to load alert data: {message}', + values: { + message: e.message, + }, + } + ), + }); + } +} + +export const AlertDataRouteWithApi = withBulkAlertOperations(AlertDataRoute); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 983fe5641e62b1..571d28a7f1c4b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -28,6 +28,8 @@ import { unmuteAlertInstance, loadAlert, loadAlertState, + loadAlertData, + AlertData, loadAlertInstanceSummary, loadAlertTypes, alertingFrameworkHealth, @@ -60,6 +62,7 @@ export interface ComponentOpts { loadAlert: (id: Alert['id']) => Promise; loadAlertState: (id: Alert['id']) => Promise; loadAlertInstanceSummary: (id: Alert['id']) => Promise; + loadAlertData: (id: Alert['id']) => Promise; loadAlertTypes: () => Promise; getHealth: () => Promise; } @@ -131,6 +134,7 @@ export function withBulkAlertOperations( loadAlertInstanceSummary={async (alertId: Alert['id']) => loadAlertInstanceSummary({ http, alertId }) } + loadAlertData={async (alertId: Alert['id']) => loadAlertData({ http, alertId })} loadAlertTypes={async () => loadAlertTypes({ http })} getHealth={async () => alertingFrameworkHealth({ http })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1fd031cda6d961..9f685e043dcf88 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -241,6 +241,7 @@ export interface AlertTypeModel>>; requiresAppContext: boolean; defaultActionMessage?: string; + formatter?: (opts: any) => string; } export interface IErrorObject {