diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts index dc260578641f8b..049900bd30f4db 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts +++ b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts @@ -40,7 +40,7 @@ const getMockRule = () => { error: null, }, monitoring: { - execution: { + run: { history: [ { success: true, diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx index ed2a1d7b17e14e..cde89f97e53da2 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx +++ b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx @@ -88,6 +88,7 @@ export const StorybookContextDecorator: React.FC ruleTagFilter: true, ruleStatusFilter: true, rulesDetailLogs: true, + ruleLastRunOutcome: true, }, }); return ( diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 8c7e5a500bf0e3..6a0f9b1c8f1403 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -17,6 +17,7 @@ export const allowedExperimentalValues = Object.freeze({ ruleTagFilter: true, ruleStatusFilter: true, rulesDetailLogs: true, + ruleLastRunOutcome: true, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx index 31c248ae1254ed..468e9a0cda4c77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx @@ -77,6 +77,7 @@ interface UseBulkEditSelectProps { actionTypesFilter?: string[]; tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; + ruleLastRunOutcomesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; searchText?: string; } @@ -89,6 +90,7 @@ export function useBulkEditSelect(props: UseBulkEditSelectProps) { actionTypesFilter, tagsFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, searchText, } = props; @@ -187,6 +189,7 @@ export function useBulkEditSelect(props: UseBulkEditSelectProps) { actionTypesFilter, tagsFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, searchText, }); @@ -208,6 +211,7 @@ export function useBulkEditSelect(props: UseBulkEditSelectProps) { actionTypesFilter, tagsFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, searchText, ] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts index a3516c4c71461c..06cd5465963382 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts @@ -7,11 +7,21 @@ import { i18n } from '@kbn/i18n'; import { useState, useCallback, useMemo } from 'react'; -import { RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; +import { RuleExecutionStatusValues, RuleLastRunOutcomeValues } from '@kbn/alerting-plugin/common'; import type { LoadRuleAggregationsProps } from '../lib/rule_api'; import { loadRuleAggregationsWithKueryFilter } from '../lib/rule_api/aggregate_kuery_filter'; import { useKibana } from '../../common/lib/kibana'; +const initializeAggregationResult = (values: readonly string[]) => { + return values.reduce>( + (prev: Record, status: string) => ({ + ...prev, + [status]: 0, + }), + {} + ); +}; + type UseLoadRuleAggregationsProps = Omit & { onError: (message: string) => void; }; @@ -21,6 +31,7 @@ export function useLoadRuleAggregations({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, onError, @@ -28,15 +39,13 @@ export function useLoadRuleAggregations({ const { http } = useKibana().services; const [rulesStatusesTotal, setRulesStatusesTotal] = useState>( - RuleExecutionStatusValues.reduce>( - (prev: Record, status: string) => ({ - ...prev, - [status]: 0, - }), - {} - ) + initializeAggregationResult(RuleExecutionStatusValues) ); + const [rulesLastRunOutcomesTotal, setRulesLastRunOutcomesTotal] = useState< + Record + >(initializeAggregationResult(RuleLastRunOutcomeValues)); + const internalLoadRuleAggregations = useCallback(async () => { try { const rulesAggs = await loadRuleAggregationsWithKueryFilter({ @@ -45,12 +54,16 @@ export function useLoadRuleAggregations({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); } + if (rulesAggs?.ruleLastRunOutcome) { + setRulesLastRunOutcomesTotal(rulesAggs.ruleLastRunOutcome); + } } catch (e) { onError( i18n.translate( @@ -67,18 +80,28 @@ export function useLoadRuleAggregations({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, onError, setRulesStatusesTotal, + setRulesLastRunOutcomesTotal, ]); return useMemo( () => ({ loadRuleAggregations: internalLoadRuleAggregations, rulesStatusesTotal, + rulesLastRunOutcomesTotal, setRulesStatusesTotal, + setRulesLastRunOutcomesTotal, }), - [internalLoadRuleAggregations, rulesStatusesTotal, setRulesStatusesTotal] + [ + internalLoadRuleAggregations, + rulesStatusesTotal, + rulesLastRunOutcomesTotal, + setRulesStatusesTotal, + setRulesLastRunOutcomesTotal, + ] ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts index 072c539ae90f58..45ac0a1d3c3b51 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts @@ -89,6 +89,7 @@ export function useLoadRules({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, sort, @@ -120,6 +121,7 @@ export function useLoadRules({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, sort, @@ -144,6 +146,7 @@ export function useLoadRules({ hasEmptyTypesFilter && isEmpty(actionTypesFilter) && isEmpty(ruleExecutionStatusesFilter) && + isEmpty(ruleLastRunOutcomesFilter) && isEmpty(ruleStatusesFilter) && isEmpty(tagsFilter) ); @@ -168,6 +171,7 @@ export function useLoadRules({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, sort, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts index 7f63fcbaa30495..cc236ef876ee6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts @@ -15,6 +15,7 @@ export interface RuleTagsAggregations { export const rewriteBodyRes: RewriteRequestCase = ({ rule_execution_status: ruleExecutionStatus, + rule_last_run_outcome: ruleLastRunOutcome, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, @@ -26,6 +27,7 @@ export const rewriteBodyRes: RewriteRequestCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleLastRunOutcome, ruleTags, }); @@ -41,6 +43,7 @@ export interface LoadRuleAggregationsProps { typesFilter?: string[]; actionTypesFilter?: string[]; ruleExecutionStatusesFilter?: string[]; + ruleLastRunOutcomesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; tagsFilter?: string[]; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts index b5afe76889a26c..9f514893d1836c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts @@ -6,7 +6,7 @@ */ import { RuleExecutionStatus } from '@kbn/alerting-plugin/common'; import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; -import { Rule, RuleAction, ResolvedRule } from '../../../types'; +import { Rule, RuleAction, ResolvedRule, RuleLastRun } from '../../../types'; const transformAction: RewriteRequestCase = ({ group, @@ -30,6 +30,16 @@ const transformExecutionStatus: RewriteRequestCase = ({ ...rest, }); +const transformLastRun: RewriteRequestCase = ({ + outcome_msg: outcomeMsg, + alerts_count: alertsCount, + ...rest +}) => ({ + outcomeMsg, + alertsCount, + ...rest, +}); + export const transformRule: RewriteRequestCase = ({ rule_type_id: ruleTypeId, created_by: createdBy, @@ -46,6 +56,8 @@ export const transformRule: RewriteRequestCase = ({ snooze_schedule: snoozeSchedule, is_snoozed_until: isSnoozedUntil, active_snoozes: activeSnoozes, + last_run: lastRun, + next_run: nextRun, ...rest }: any) => ({ ruleTypeId, @@ -65,6 +77,8 @@ export const transformRule: RewriteRequestCase = ({ scheduledTaskId, isSnoozedUntil, activeSnoozes, + ...(lastRun ? { lastRun: transformLastRun(lastRun) } : {}), + ...(nextRun ? { nextRun } : {}), ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts index 4e37f9a02fb808..c04b55ff6db499 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts @@ -12,7 +12,13 @@ import { transformRule } from './common_transformations'; type RuleCreateBody = Omit< RuleUpdates, - 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' + | 'createdBy' + | 'updatedBy' + | 'muteAll' + | 'mutedInstanceIds' + | 'executionStatus' + | 'lastRun' + | 'nextRun' >; const rewriteBodyRequest: RewriteResponseCase = ({ ruleTypeId, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kuery_node.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kuery_node.ts index a49b0a489bfec7..8159501b3d4b4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kuery_node.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kuery_node.ts @@ -12,6 +12,7 @@ export const mapFiltersToKueryNode = ({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, searchText, @@ -20,6 +21,7 @@ export const mapFiltersToKueryNode = ({ actionTypesFilter?: string[]; tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; + ruleLastRunOutcomesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; searchText?: string; }): KueryNode | null => { @@ -51,6 +53,16 @@ export const mapFiltersToKueryNode = ({ ); } + if (ruleLastRunOutcomesFilter && ruleLastRunOutcomesFilter.length) { + filterKueryNode.push( + nodeBuilder.or( + ruleLastRunOutcomesFilter.map((resf) => + nodeBuilder.is('alert.attributes.lastRun.outcome', resf) + ) + ) + ); + } + if (ruleStatusesFilter && ruleStatusesFilter.length) { const snoozedFilter = nodeBuilder.or([ fromKueryExpression('alert.attributes.muteAll: true'), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts index dab59919092a2d..cce9fd3e5ac979 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts @@ -18,6 +18,7 @@ export interface LoadRulesProps { actionTypesFilter?: string[]; tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; + ruleLastRunOutcomesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts index 171b514429cd74..46d2c32ccdbddf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts @@ -18,6 +18,7 @@ export async function loadRulesWithKueryFilter({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, sort = { field: 'name', direction: 'asc' }, @@ -32,6 +33,7 @@ export async function loadRulesWithKueryFilter({ actionTypesFilter, tagsFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, searchText, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 02c740d2b6cc8c..159a949364fde8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -8,11 +8,7 @@ import React, { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui'; -import { - ActionGroup, - RuleExecutionStatusErrorReasons, - AlertStatusValues, -} from '@kbn/alerting-plugin/common'; +import { ActionGroup, AlertStatusValues } from '@kbn/alerting-plugin/common'; import { useKibana } from '../../../../common/lib/kibana'; import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types'; import { @@ -20,15 +16,14 @@ import { withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; import './rule.scss'; -import { getHealthColor } from '../../rules_list/components/rule_execution_status_filter'; -import { - rulesStatusesTranslationsMapping, - ALERT_STATUS_LICENSE_ERROR, -} from '../../rules_list/translations'; import type { RuleEventLogListProps } from './rule_event_log_list'; import { AlertListItem } from './types'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props'; +import { + getRuleHealthColor, + getRuleStatusMessage, +} from '../../../../common/lib/rule_status_helpers'; import RuleStatusPanelWithApi from './rule_status_panel'; const RuleEventLogList = lazy(() => import('./rule_event_log_list')); @@ -78,12 +73,8 @@ export function RuleComponent({ requestRefresh(); }; - const healthColor = getHealthColor(rule.executionStatus.status); - const isLicenseError = - rule.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; - const statusMessage = isLicenseError - ? ALERT_STATUS_LICENSE_ERROR - : rulesStatusesTranslationsMapping[rule.executionStatus.status]; + const healthColor = getRuleHealthColor(rule); + const statusMessage: string = getRuleStatusMessage(rule); const renderRuleAlertList = () => { return suspendedComponentWithProps( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx index 20f3612f3a41be..c7b4c84f64c30d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_data_grid.tsx @@ -30,6 +30,7 @@ import { executionLogSortableColumns, ExecutionLogSortFields, } from '@kbn/alerting-plugin/common'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { RuleEventLogListCellRenderer, ColumnId } from './rule_event_log_list_cell_renderer'; import { RuleEventLogPaginationStatus } from './rule_event_log_pagination_status'; import { RuleActionErrorBadge } from './rule_action_error_badge'; @@ -174,6 +175,8 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { const { euiTheme } = useEuiTheme(); + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + const getPaginatedRowIndex = useCallback( (rowIndex: number) => { const { pageIndex, pageSize } = pagination; @@ -621,6 +624,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => { dateFormat={dateFormat} ruleId={ruleId} spaceIds={spaceIds} + lastRunOutcomeEnabled={isRuleLastRunOutcomeEnabled} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx index bcca56ad0027ed..d3991e9b1505fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_cell_renderer.tsx @@ -32,10 +32,19 @@ interface RuleEventLogListCellRendererProps { dateFormat?: string; ruleId?: string; spaceIds?: string[]; + lastRunOutcomeEnabled?: boolean; } export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRendererProps) => { - const { columnId, value, version, dateFormat = DEFAULT_DATE_FORMAT, ruleId, spaceIds } = props; + const { + columnId, + value, + version, + dateFormat = DEFAULT_DATE_FORMAT, + ruleId, + spaceIds, + lastRunOutcomeEnabled = false, + } = props; const spacesData = useSpacesData(); const { http } = useKibana().services; @@ -85,7 +94,12 @@ export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRenderer } if (columnId === 'status') { - return ; + return ( + + ); } if (columnId === 'timestamp') { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx index c51109bd11514d..718222636830a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx @@ -11,6 +11,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { loadExecutionKPIAggregations } from '../../../lib/rule_api/load_execution_kpi_aggregations'; import { loadGlobalExecutionKPIAggregations } from '../../../lib/rule_api/load_global_execution_kpi_aggregations'; import { RuleEventLogListKPI } from './rule_event_log_list_kpi'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; jest.mock('../../../../common/lib/kibana', () => ({ useKibana: jest.fn().mockReturnValue({ @@ -28,6 +29,10 @@ jest.mock('../../../lib/rule_api/load_global_execution_kpi_aggregations', () => loadGlobalExecutionKPIAggregations: jest.fn(), })); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn(), +})); + const mockKpiResponse = { success: 4, unknown: 0, @@ -48,6 +53,7 @@ const loadGlobalExecutionKPIAggregationsMock = describe('rule_event_log_list_kpi', () => { beforeEach(() => { jest.clearAllMocks(); + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); loadExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); loadGlobalExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx index 0696f857261ec7..783b985c973462 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx @@ -14,6 +14,7 @@ import { ComponentOpts as RuleApis, withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; import { RuleEventLogListStatus } from './rule_event_log_list_status'; @@ -104,6 +105,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { } = useKibana().services; const isInitialized = useRef(false); + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); const [isLoading, setIsLoading] = useState(false); const [kpi, setKpi] = useState(); @@ -168,7 +170,12 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { )} + description={getStatDescription( + + )} titleSize="s" title={kpi?.success ?? 0} isLoading={isLoadingData} @@ -177,7 +184,12 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { )} + description={getStatDescription( + + )} titleSize="s" title={kpi?.warning ?? 0} isLoading={isLoadingData} @@ -186,7 +198,12 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { )} + description={getStatDescription( + + )} titleSize="s" title={kpi?.failure ?? 0} isLoading={isLoadingData} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status.tsx index d82915f2fd59d0..80fd7daf0dd64b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiIcon } from '@elastic/eui'; import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common'; interface RuleEventLogListStatusProps { status: RuleAlertingOutcome; + lastRunOutcomeEnabled?: boolean; } const statusContainerStyles = { @@ -30,14 +31,28 @@ const STATUS_TO_COLOR: Record = { warning: 'warning', }; +const STATUS_TO_OUTCOME: Record = { + success: 'succeeded', + failure: 'failed', + warning: 'warning', + unknown: 'unknown', +}; + export const RuleEventLogListStatus = (props: RuleEventLogListStatusProps) => { - const { status } = props; + const { status, lastRunOutcomeEnabled = false } = props; const color = STATUS_TO_COLOR[status] || 'gray'; + const statusString = useMemo(() => { + if (lastRunOutcomeEnabled) { + return STATUS_TO_OUTCOME[status]; + } + return status; + }, [lastRunOutcomeEnabled, status]); + return (
- {status} + {statusString}
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.test.tsx index 14591c7a15ccd6..c4bbc7e7205001 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.test.tsx @@ -9,6 +9,15 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; + +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn(), +})); + +beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); const onChangeMock = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.tsx index 6d1b38a7ae94de..af4325c906456e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_status_filter.tsx @@ -9,6 +9,7 @@ import React, { useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common'; import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { RuleEventLogListStatus } from './rule_event_log_list_status'; const statusFilters: RuleAlertingOutcome[] = ['success', 'failure', 'warning', 'unknown']; @@ -21,6 +22,8 @@ interface RuleEventLogListStatusFilterProps { export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilterProps) => { const { selectedOptions = [], onChange = () => {} } = props; + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onFilterItemClick = useCallback( @@ -68,7 +71,10 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter onClick={onFilterItemClick(status)} checked={selectedOptions.includes(status) ? 'on' : undefined} > - + ); })} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx index e5bb7ffd1b0e42..76451421c9c8e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiHealth } from '@elastic/eui'; import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; import { rulesStatusesTranslationsMapping } from '../translations'; +import { getExecutionStatusHealthColor } from '../../../../common/lib'; interface RuleExecutionStatusFilterProps { selectedStatuses: string[]; @@ -66,7 +67,7 @@ export const RuleExecutionStatusFilter: React.FunctionComponent
{sortedRuleExecutionStatusValues.map((item: RuleExecutionStatuses) => { - const healthColor = getHealthColor(item); + const healthColor = getExecutionStatusHealthColor(item); return ( void; +} + +export const RuleLastRunOutcomeFilter: React.FunctionComponent = ({ + selectedOutcomes, + onChange, +}: RuleLastRunOutcomeFilterProps) => { + const [selectedValues, setSelectedValues] = useState(selectedOutcomes); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onTogglePopover = useCallback(() => { + setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + }, [setIsPopoverOpen]); + + const onClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, [setIsPopoverOpen]); + + useEffect(() => { + if (onChange) { + onChange(selectedValues); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedValues]); + + useEffect(() => { + setSelectedValues(selectedOutcomes); + }, [selectedOutcomes]); + + return ( + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={onTogglePopover} + data-test-subj="ruleLastRunOutcomeFilterButton" + > + + + } + > +
+ {sortedRuleLastRunOutcomeValues.map((item: RuleLastRunOutcomes) => { + const healthColor = getOutcomeHealthColor(item); + return ( + { + const isPreviouslyChecked = selectedValues.includes(item); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item)); + } else { + setSelectedValues(selectedValues.concat(item)); + } + }} + checked={selectedValues.includes(item) ? 'on' : undefined} + data-test-subj={`ruleLastRunOutcome${item}FilterOption`} + > + + {rulesLastRunOutcomeTranslationMapping[item]} + + + ); + })} +
+
+ ); +}; + +export { getOutcomeHealthColor as getHealthColor }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 938349702f90c4..ef44ec07656f7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -358,6 +358,14 @@ describe('rules_list component with props', () => { }); describe('Last response filter', () => { + beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + }); + + afterEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); + }); + let wrapper: ReactWrapper; async function setup(editable: boolean = true) { loadRulesWithKueryFilter.mockResolvedValue({ @@ -408,7 +416,7 @@ describe('rules_list component with props', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; wrapper = mountWithIntl( - + ); await act(async () => { await nextTick(); @@ -420,49 +428,48 @@ describe('rules_list component with props', () => { expect(loadRuleAggregationsWithKueryFilter).toHaveBeenCalled(); } it('can filter by last response', async () => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); loadRulesWithKueryFilter.mockReset(); await setup(); expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith( expect.objectContaining({ - ruleExecutionStatusesFilter: ['error'], + ruleLastRunOutcomesFilter: ['failed'], }) ); - wrapper.find('[data-test-subj="ruleExecutionStatusFilterButton"] button').simulate('click'); + wrapper.find('[data-test-subj="ruleLastRunOutcomeFilterButton"] button').simulate('click'); wrapper - .find('[data-test-subj="ruleExecutionStatusactiveFilterOption"]') + .find('[data-test-subj="ruleLastRunOutcomesucceededFilterOption"]') .first() .simulate('click'); expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith( expect.objectContaining({ - ruleExecutionStatusesFilter: ['error', 'active'], + ruleLastRunOutcomesFilter: ['failed', 'succeeded'], }) ); - expect(wrapper.prop('onLastResponseFilterChange')).toHaveBeenCalled(); - expect(wrapper.prop('onLastResponseFilterChange')).toHaveBeenLastCalledWith([ - 'error', - 'active', + expect(wrapper.prop('onLastRunOutcomeFilterChange')).toHaveBeenCalled(); + expect(wrapper.prop('onLastRunOutcomeFilterChange')).toHaveBeenLastCalledWith([ + 'failed', + 'succeeded', ]); - wrapper.find('[data-test-subj="ruleExecutionStatusFilterButton"] button').simulate('click'); + wrapper.find('[data-test-subj="ruleLastRunOutcomeFilterButton"] button').simulate('click'); wrapper - .find('[data-test-subj="ruleExecutionStatuserrorFilterOption"]') + .find('[data-test-subj="ruleLastRunOutcomefailedFilterOption"]') .first() .simulate('click'); expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith( expect.objectContaining({ - ruleExecutionStatusesFilter: ['active'], + ruleLastRunOutcomesFilter: ['succeeded'], }) ); - expect(wrapper.prop('onLastResponseFilterChange')).toHaveBeenCalled(); - expect(wrapper.prop('onLastResponseFilterChange')).toHaveBeenLastCalledWith(['active']); + expect(wrapper.prop('onLastRunOutcomeFilterChange')).toHaveBeenCalled(); + expect(wrapper.prop('onLastRunOutcomeFilterChange')).toHaveBeenLastCalledWith(['succeeded']); }); }); @@ -844,6 +851,11 @@ describe('rules_list component with items', () => { loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); loadAllActions.mockResolvedValue([]); loadRuleAggregationsWithKueryFilter.mockResolvedValue({ + ruleLastRunOutcome: { + succeeded: 3, + failed: 3, + warning: 6, + }, ruleEnabledStatus: { enabled: 2, disabled: 0 }, ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, ruleMutedStatus: { muted: 0, unmuted: 2 }, @@ -1005,11 +1017,9 @@ describe('rules_list component with items', () => { expect( wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length ).toEqual(mockedRulesData.length); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); + + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-succeeded"]').length).toEqual(2); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-failed"]').length).toEqual(2); expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1); expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); expect( @@ -1018,10 +1028,10 @@ describe('rules_list component with items', () => { expect(wrapper.find('[data-test-subj="rulesListAutoRefresh"]').exists()).toBeTruthy(); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-failed"]').first().text()).toEqual( 'Error' ); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-failed"]').last().text()).toEqual( 'License Error' ); }); @@ -1042,7 +1052,7 @@ describe('rules_list component with items', () => { mockedRulesData.forEach((rule, index) => { if (rule.monitoring) { expect(ratios.at(index).text()).toEqual( - `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` + `${rule.monitoring.run.calculated_metrics.success_ratio * 100}%` ); } else { expect(ratios.at(index).text()).toEqual(`N/A`); @@ -1060,10 +1070,10 @@ describe('rules_list component with items', () => { ); mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { + if (typeof rule.monitoring?.run.calculated_metrics.p50 === 'number') { // Ensure the table cells are getting the correct values expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) + getFormattedDuration(rule.monitoring.run.calculated_metrics.p50) ); // Ensure the tooltip is showing the correct content expect( @@ -1073,7 +1083,7 @@ describe('rules_list component with items', () => { ) .at(index) .props().content - ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); + ).toEqual(getFormattedMilliseconds(rule.monitoring.run.calculated_metrics.p50)); } else { expect(percentiles.at(index).text()).toEqual('N/A'); } @@ -1149,9 +1159,9 @@ describe('rules_list component with items', () => { ); mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { + if (typeof rule.monitoring?.run.calculated_metrics.p95 === 'number') { expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) + getFormattedDuration(rule.monitoring.run.calculated_metrics.p95) ); } else { expect(percentiles.at(index).text()).toEqual('N/A'); @@ -1270,21 +1280,19 @@ describe('rules_list component with items', () => { }); it('renders brief', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); await setup(); - // { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 } - expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1'); - expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual( - 'Active: 2' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual( - 'Error: 3' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual( - 'Pending: 4' + // ruleLastRunOutcome: { + // succeeded: 3, + // failed: 3, + // warning: 6, + // } + expect(wrapper.find('EuiHealth[data-test-subj="totalSucceededRulesCount"]').text()).toEqual( + 'Succeeded: 3' ); - expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual( - 'Unknown: 5' + expect(wrapper.find('EuiHealth[data-test-subj="totalFailedRulesCount"]').text()).toEqual( + 'Failed: 3' ); expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual( 'Warning: 6' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index ed5bac05c4edef..fffc395689bd6d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -22,13 +22,10 @@ import { EuiSpacer, EuiLink, EuiEmptyPrompt, - EuiHealth, EuiTableSortingType, EuiButtonIcon, EuiSelectableOption, - EuiIcon, EuiDescriptionList, - EuiCallOut, } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { useHistory } from 'react-router-dom'; @@ -55,9 +52,12 @@ import { RuleAdd, RuleEdit } from '../../rule_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { RuleQuickEditButtonsWithApi as RuleQuickEditButtons } from '../../common/components/rule_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; +import { RulesListStatuses } from './rules_list_statuses'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { RuleExecutionStatusFilter } from './rule_execution_status_filter'; +import { RuleLastRunOutcomeFilter } from './rule_last_run_outcome_filter'; +import { RulesListErrorBanner } from './rules_list_error_banner'; import { loadRuleTypes, disableRule, @@ -116,6 +116,8 @@ export interface RulesListProps { onStatusFilterChange?: (status: RuleStatus[]) => RulesPageContainerState; lastResponseFilter?: string[]; onLastResponseFilterChange?: (lastResponse: string[]) => RulesPageContainerState; + lastRunOutcomeFilter?: string[]; + onLastRunOutcomeFilterChange?: (lastRunOutcome: string[]) => RulesPageContainerState; refresh?: Date; rulesListKey?: string; visibleColumns?: RulesListVisibleColumns[]; @@ -128,9 +130,9 @@ interface RuleTypeState { } export const percentileFields = { - [Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50', - [Percentiles.P95]: 'monitoring.execution.calculated_metrics.p95', - [Percentiles.P99]: 'monitoring.execution.calculated_metrics.p99', + [Percentiles.P50]: 'monitoring.run.calculated_metrics.p50', + [Percentiles.P95]: 'monitoring.run.calculated_metrics.p95', + [Percentiles.P99]: 'monitoring.run.calculated_metrics.p99', }; const initialPercentileOptions = Object.values(Percentiles).map((percentile) => ({ @@ -148,6 +150,8 @@ export const RulesList = ({ onStatusFilterChange, lastResponseFilter, onLastResponseFilterChange, + lastRunOutcomeFilter, + onLastRunOutcomeFilterChange, refresh, rulesListKey, visibleColumns, @@ -174,6 +178,9 @@ export const RulesList = ({ const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState( lastResponseFilter || [] ); + const [ruleLastRunOutcomesFilter, setRuleLastRunOutcomesFilter] = useState( + lastRunOutcomeFilter || [] + ); const [ruleStatusesFilter, setRuleStatusesFilter] = useState(statusFilter || []); const [tagsFilter, setTagsFilter] = useState([]); @@ -188,6 +195,7 @@ export const RulesList = ({ const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter'); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); useEffect(() => { (async () => { @@ -277,6 +285,7 @@ export const RulesList = ({ typesFilter: rulesTypesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, sort, @@ -289,15 +298,17 @@ export const RulesList = ({ onError, }); - const { loadRuleAggregations, rulesStatusesTotal } = useLoadRuleAggregations({ - searchText, - typesFilter, - actionTypesFilter, - ruleExecutionStatusesFilter, - ruleStatusesFilter, - tagsFilter, - onError, - }); + const { loadRuleAggregations, rulesStatusesTotal, rulesLastRunOutcomesTotal } = + useLoadRuleAggregations({ + searchText, + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, + ruleStatusesFilter, + tagsFilter, + onError, + }); const onRuleEdit = (ruleItem: RuleTableItem) => { setEditFlyoutVisibility(true); @@ -402,12 +413,24 @@ export const RulesList = ({ } }, [lastResponseFilter]); + useEffect(() => { + if (lastRunOutcomeFilter) { + setRuleLastRunOutcomesFilter(lastRunOutcomeFilter); + } + }, [lastResponseFilter]); + useEffect(() => { if (onLastResponseFilterChange) { onLastResponseFilterChange(ruleExecutionStatusesFilter); } }, [ruleExecutionStatusesFilter]); + useEffect(() => { + if (onLastRunOutcomeFilterChange) { + onLastRunOutcomeFilterChange(ruleLastRunOutcomesFilter); + } + }, [ruleLastRunOutcomesFilter]); + // Clear bulk selection anytime the filters change useEffect(() => { onClearSelection(); @@ -416,6 +439,7 @@ export const RulesList = ({ rulesTypesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, hasDefaultRuleTypesFiltersOn, @@ -465,7 +489,7 @@ export const RulesList = ({ setShowErrors((prevValue) => { if (!prevValue) { const rulesToExpand = rulesState.data.reduce((acc, ruleItem) => { - if (ruleItem.executionStatus.status === 'error') { + if (ruleItem.lastRun?.outcome === 'failed') { return { ...acc, [ruleItem.id]: ( @@ -528,6 +552,25 @@ export const RulesList = ({ return null; }; + const getRuleOutcomeOrStatusFilter = () => { + if (isRuleLastRunOutcomeEnabled) { + return [ + , + ]; + } + return [ + , + ]; + }; + const onDisableRule = (rule: RuleTableItem) => { return disableRule({ http, id: rule.id }); }; @@ -571,11 +614,7 @@ export const RulesList = ({ filters={typesFilter} /> ), - , + ...getRuleOutcomeOrStatusFilter(), ...getRuleTagFilter(), ]; @@ -609,6 +648,7 @@ export const RulesList = ({ typesFilter: rulesTypesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleLastRunOutcomesFilter, ruleStatusesFilter, tagsFilter, }); @@ -681,34 +721,11 @@ export const RulesList = ({ const table = ( <> - {rulesStatusesTotal.error > 0 ? ( - <> - -

- -   - -   - setRuleExecutionStatusesFilter(['error'])}> - - -

-
- - - ) : null} + {authorizedToCreateAnyRules && showCreateRuleButton ? ( @@ -778,68 +795,10 @@ export const RulesList = ({ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.tsx index c88070cceafee3..39089c2168cb35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.tsx @@ -36,7 +36,8 @@ export type RulesListVisibleColumns = | 'ruleExecutionPercentile' | 'ruleExecutionSuccessRatio' | 'ruleExecutionStatus' - | 'ruleExecutionState'; + | 'ruleExecutionState' + | 'ruleLastRunOutcome'; const OriginalRulesListVisibleColumns: RulesListVisibleColumns[] = [ 'ruleName', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_error_banner.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_error_banner.tsx new file mode 100644 index 00000000000000..4d51b61f6454f1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_error_banner.tsx @@ -0,0 +1,64 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; + +interface RulesListErrorBannerProps { + rulesLastRunOutcomes: Record; + setRuleExecutionStatusesFilter: (statuses: string[]) => void; + setRuleLastRunOutcomesFilter: (outcomes: string[]) => void; +} + +export const RulesListErrorBanner = (props: RulesListErrorBannerProps) => { + const { rulesLastRunOutcomes, setRuleExecutionStatusesFilter, setRuleLastRunOutcomesFilter } = + props; + + const onClick = () => { + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + if (isRuleLastRunOutcomeEnabled) { + setRuleLastRunOutcomesFilter(['failed']); + } else { + setRuleExecutionStatusesFilter(['error']); + } + }; + + if (rulesLastRunOutcomes.failed === 0) { + return null; + } + + return ( + <> + +

+ +   + +   + + + +

+
+ + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_statuses.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_statuses.tsx new file mode 100644 index 00000000000000..1f38e8307a107f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_statuses.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EuiFlexItem, EuiHealth } from '@elastic/eui'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; + +import { + RULE_STATUS_ACTIVE, + RULE_STATUS_ERROR, + RULE_STATUS_WARNING, + RULE_STATUS_OK, + RULE_STATUS_PENDING, + RULE_STATUS_UNKNOWN, + RULE_LAST_RUN_OUTCOME_SUCCEEDED_DESCRIPTION, + RULE_LAST_RUN_OUTCOME_WARNING_DESCRIPTION, + RULE_LAST_RUN_OUTCOME_FAILED_DESCRIPTION, +} from '../translations'; + +interface RulesListStatusesProps { + rulesStatuses: Record; + rulesLastRunOutcomes: Record; +} + +export const RulesListStatuses = (props: RulesListStatusesProps) => { + const { rulesStatuses, rulesLastRunOutcomes } = props; + + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + + if (isRuleLastRunOutcomeEnabled) { + return ( + <> + + + {RULE_LAST_RUN_OUTCOME_SUCCEEDED_DESCRIPTION(rulesLastRunOutcomes.succeeded)} + + + + + {RULE_LAST_RUN_OUTCOME_FAILED_DESCRIPTION(rulesLastRunOutcomes.failed)} + + + + + {RULE_LAST_RUN_OUTCOME_WARNING_DESCRIPTION(rulesLastRunOutcomes.warning)} + + + + ); + } + + return ( + <> + + + {RULE_STATUS_ACTIVE(rulesStatuses.active)} + + + + + {RULE_STATUS_ERROR(rulesStatuses.error)} + + + + + {RULE_STATUS_WARNING(rulesStatuses.warning)} + + + + + {RULE_STATUS_OK(rulesStatuses.ok)} + + + + + {RULE_STATUS_PENDING(rulesStatuses.pending)} + + + + + {RULE_STATUS_UNKNOWN(rulesStatuses.unknown)} + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx index f171f36ebb8dd0..b0a2186c89c9e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx @@ -9,7 +9,6 @@ import moment from 'moment'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { EuiBasicTable, @@ -18,7 +17,6 @@ import { EuiIconTip, EuiLink, EuiButtonEmpty, - EuiHealth, EuiText, EuiToolTip, EuiTableSortingType, @@ -32,21 +30,17 @@ import { } from '@elastic/eui'; import { RuleExecutionStatus, - RuleExecutionStatusErrorReasons, formatDuration, parseDuration, MONITORING_HISTORY_LIMIT, } from '@kbn/alerting-plugin/common'; import { - rulesStatusesTranslationsMapping, - ALERT_STATUS_LICENSE_ERROR, SELECT_ALL_RULES, CLEAR_SELECTION, TOTAL_RULES, SELECT_ALL_ARIA_LABEL, } from '../translations'; -import { getHealthColor } from './rule_execution_status_filter'; import { Rule, RuleTableItem, @@ -67,6 +61,7 @@ import { hasAllPrivilege } from '../../../lib/capabilities'; import { RuleTagBadge } from './rule_tag_badge'; import { RuleStatusDropdown } from './rule_status_dropdown'; import { RulesListNotifyBadge } from './rules_list_notify_badge'; +import { RulesListTableStatusCell } from './rules_list_table_status_cell'; import { RulesListColumns, RulesListVisibleColumns, @@ -92,9 +87,9 @@ const percentileOrdinals = { }; export const percentileFields = { - [Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50', - [Percentiles.P95]: 'monitoring.execution.calculated_metrics.p95', - [Percentiles.P99]: 'monitoring.execution.calculated_metrics.p99', + [Percentiles.P50]: 'monitoring.run.calculated_metrics.p50', + [Percentiles.P95]: 'monitoring.run.calculated_metrics.p95', + [Percentiles.P99]: 'monitoring.run.calculated_metrics.p99', }; const EMPTY_OBJECT = {}; @@ -301,58 +296,6 @@ export const RulesListTable = (props: RulesListTableProps) => { [isRuleTypeEditableInContext, onDisableRule, onEnableRule, onRuleChanged] ); - const renderRuleExecutionStatus = useCallback( - (executionStatus: RuleExecutionStatus, rule: RuleTableItem) => { - const healthColor = getHealthColor(executionStatus.status); - const tooltipMessage = - executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null; - const isLicenseError = - executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; - const statusMessage = isLicenseError - ? ALERT_STATUS_LICENSE_ERROR - : rulesStatusesTranslationsMapping[executionStatus.status]; - - const health = ( - - {statusMessage} - - ); - - const healthWithTooltip = tooltipMessage ? ( - - {health} - - ) : ( - health - ); - - return ( - - {healthWithTooltip} - {isLicenseError && ( - - onManageLicenseClick(rule)} - > - - - - )} - - ); - }, - [onManageLicenseClick] - ); - const selectionColumn = useMemo(() => { return { id: 'ruleSelection', @@ -684,7 +627,7 @@ export const RulesListTable = (props: RulesListTableProps) => { }, { id: 'ruleExecutionSuccessRatio', - field: 'monitoring.execution.calculated_metrics.success_ratio', + field: 'monitoring.run.calculated_metrics.success_ratio', width: '12%', selectorName: i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.selector.successRatioTitle', @@ -729,7 +672,9 @@ export const RulesListTable = (props: RulesListTableProps) => { width: '120px', 'data-test-subj': 'rulesTableCell-lastResponse', render: (_executionStatus: RuleExecutionStatus, rule: RuleTableItem) => { - return renderRuleExecutionStatus(rule.executionStatus, rule); + return ( + + ); }, }, { @@ -827,11 +772,11 @@ export const RulesListTable = (props: RulesListTableProps) => { onRuleEditClick, onSnoozeRule, onUnsnoozeRule, + onManageLicenseClick, renderCollapsedItemActions, renderPercentileCellValue, renderPercentileColumnName, renderRuleError, - renderRuleExecutionStatus, renderRuleStatusDropdown, ruleTypesState.data, selectedPercentile, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table_status_cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table_status_cell.tsx new file mode 100644 index 00000000000000..7a0d5ff3e79a8f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table_status_cell.tsx @@ -0,0 +1,69 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiHealth, EuiToolTip } from '@elastic/eui'; +import { RuleTableItem } from '../../../../types'; +import { + getRuleHealthColor, + getIsLicenseError, + getRuleStatusMessage, +} from '../../../../common/lib/rule_status_helpers'; + +interface RulesListTableStatusCellProps { + rule: RuleTableItem; + onManageLicenseClick: (rule: RuleTableItem) => void; +} + +export const RulesListTableStatusCell = (props: RulesListTableStatusCellProps) => { + const { rule, onManageLicenseClick } = props; + const { lastRun } = rule; + + const isLicenseError = getIsLicenseError(rule); + const healthColor = getRuleHealthColor(rule); + const statusMessage = getRuleStatusMessage(rule); + const tooltipMessage = lastRun?.outcome === 'failed' ? `Error: ${lastRun?.outcomeMsg}` : null; + + if (!statusMessage) { + return null; + } + + const health = ( + + {statusMessage} + + ); + + const healthWithTooltip = tooltipMessage ? ( + + {health} + + ) : ( + health + ); + + return ( + + {healthWithTooltip} + {isLicenseError && ( + + onManageLicenseClick(rule)} + > + + + + )} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/test_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/test_helpers.ts index 8198d974a85923..545b7d7141a926 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/test_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/test_helpers.ts @@ -36,7 +36,7 @@ export const mockedRulesData = [ error: null, }, monitoring: { - execution: { + run: { history: [ { success: true, @@ -57,8 +57,18 @@ export const mockedRulesData = [ p95: 300000, p99: 300000, }, + last_run: { + timestamp: '2020-08-20T19:23:38Z', + metrics: { + duration: 500, + }, + }, }, }, + lastRun: { + outcome: 'succeeded', + alertsCount: {}, + }, }, { id: '2', @@ -83,7 +93,7 @@ export const mockedRulesData = [ error: null, }, monitoring: { - execution: { + run: { history: [ { success: true, @@ -100,8 +110,18 @@ export const mockedRulesData = [ p95: 100000, p99: 500000, }, + last_run: { + timestamp: '2020-08-20T19:23:38Z', + metrics: { + duration: 61000, + }, + }, }, }, + lastRun: { + outcome: 'succeeded', + alertsCount: {}, + }, }, { id: '3', @@ -126,11 +146,17 @@ export const mockedRulesData = [ error: null, }, monitoring: { - execution: { + run: { history: [{ success: false, duration: 100 }], calculated_metrics: { success_ratio: 0, }, + last_run: { + timestamp: '2020-08-20T19:23:38Z', + metrics: { + duration: 30234, + }, + }, }, }, }, @@ -159,6 +185,11 @@ export const mockedRulesData = [ message: 'test', }, }, + lastRun: { + outcome: 'failed', + outcomeMsg: 'test', + warning: RuleExecutionStatusErrorReasons.Unknown, + }, }, { id: '5', @@ -185,6 +216,11 @@ export const mockedRulesData = [ message: 'test', }, }, + lastRun: { + outcome: 'failed', + outcomeMsg: 'test', + warning: RuleExecutionStatusErrorReasons.License, + }, }, { id: '6', @@ -211,6 +247,11 @@ export const mockedRulesData = [ message: 'test', }, }, + lastRun: { + outcome: 'warning', + outcomeMsg: 'test', + warning: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + }, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts index e69107e060daf2..fb3bef9a1b5ff8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts @@ -55,6 +55,26 @@ export const ALERT_STATUS_WARNING = i18n.translate( defaultMessage: 'Warning', } ); +export const RULE_LAST_RUN_OUTCOME_SUCCEEDED = i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.ruleLastRunOutcomeSucceeded', + { + defaultMessage: 'Succeeded', + } +); + +export const RULE_LAST_RUN_OUTCOME_WARNING = i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.ruleLastRunOutcomeWarning', + { + defaultMessage: 'Warning', + } +); + +export const RULE_LAST_RUN_OUTCOME_FAILED = i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.ruleLastRunOutcomeFailed', + { + defaultMessage: 'Failed', + } +); export const rulesStatusesTranslationsMapping = { ok: ALERT_STATUS_OK, @@ -65,6 +85,12 @@ export const rulesStatusesTranslationsMapping = { warning: ALERT_STATUS_WARNING, }; +export const rulesLastRunOutcomeTranslationMapping = { + succeeded: RULE_LAST_RUN_OUTCOME_SUCCEEDED, + warning: RULE_LAST_RUN_OUTCOME_WARNING, + failed: RULE_LAST_RUN_OUTCOME_FAILED, +}; + export const ALERT_ERROR_UNKNOWN_REASON = i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonUnknown', { @@ -199,6 +225,93 @@ export const CLEAR_SELECTION = i18n.translate( } ); +export const RULE_STATUS_ACTIVE = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.totalStatusesActiveDescription', + { + defaultMessage: 'Active: {total}', + values: { total }, + } + ); +}; + +export const RULE_STATUS_ERROR = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.totalStatusesErrorDescription', + { + defaultMessage: 'Error: {total}', + values: { total }, + } + ); +}; + +export const RULE_STATUS_WARNING = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.totalStatusesWarningDescription', + { + defaultMessage: 'Warning: {total}', + values: { total }, + } + ); +}; + +export const RULE_STATUS_OK = (total: number) => { + return i18n.translate('xpack.triggersActionsUI.sections.rulesList.totalStatusesOkDescription', { + defaultMessage: 'Ok: {total}', + values: { total }, + }); +}; + +export const RULE_STATUS_PENDING = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.totalStatusesPendingDescription', + { + defaultMessage: 'Pending: {total}', + values: { total }, + } + ); +}; + +export const RULE_STATUS_UNKNOWN = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.totalStatusesUnknownDescription', + { + defaultMessage: 'Unknown: {total}', + values: { total }, + } + ); +}; + +export const RULE_LAST_RUN_OUTCOME_SUCCEEDED_DESCRIPTION = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.lastRunOutcomeSucceededDescription', + { + defaultMessage: 'Succeeded: {total}', + values: { total }, + } + ); +}; + +export const RULE_LAST_RUN_OUTCOME_WARNING_DESCRIPTION = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.lastRunOutcomeWarningDescription', + { + defaultMessage: 'Warning: {total}', + values: { total }, + } + ); +}; + +export const RULE_LAST_RUN_OUTCOME_FAILED_DESCRIPTION = (total: number) => { + return i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.lastRunOutcomeFailedDescription', + { + defaultMessage: 'Failed: {total}', + values: { total }, + } + ); +}; + export const SINGLE_RULE_TITLE = i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.singleTitle', { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx index 18e2f240dac215..a35599e9927861 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx @@ -20,6 +20,7 @@ describe('getIsExperimentalFeatureEnabled', () => { rulesDetailLogs: true, ruleTagFilter: true, ruleStatusFilter: true, + ruleLastRunOutcome: true, }, }); @@ -43,6 +44,10 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + + expect(result).toEqual(true); + expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError( `Invalid enable value doesNotExist. Allowed values are: ${allowedExperimentalValueKeys.join( ', ' diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts index f7af1fbbde257c..aa5fe263e084fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts @@ -6,4 +6,11 @@ */ export { getTimeFieldOptions, getTimeOptions } from './get_time_options'; +export { + getOutcomeHealthColor, + getExecutionStatusHealthColor, + getRuleHealthColor, + getIsLicenseError, + getRuleStatusMessage, +} from './rule_status_helpers'; export { useKibana } from './kibana'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/rule_status_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/rule_status_helpers.ts new file mode 100644 index 00000000000000..be450e650adf80 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/rule_status_helpers.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + RuleLastRunOutcomes, + RuleExecutionStatuses, + RuleExecutionStatusErrorReasons, +} from '@kbn/alerting-plugin/common'; +import { getIsExperimentalFeatureEnabled } from '../get_experimental_features'; +import { Rule } from '../../types'; +import { + rulesLastRunOutcomeTranslationMapping, + rulesStatusesTranslationsMapping, + ALERT_STATUS_LICENSE_ERROR, +} from '../../application/sections/rules_list/translations'; + +export const getOutcomeHealthColor = (status: RuleLastRunOutcomes) => { + switch (status) { + case 'succeeded': + return 'success'; + case 'failed': + return 'danger'; + case 'warning': + return 'warning'; + default: + return 'subdued'; + } +}; + +export const getExecutionStatusHealthColor = (status: RuleExecutionStatuses) => { + switch (status) { + case 'active': + return 'success'; + case 'error': + return 'danger'; + case 'ok': + return 'primary'; + case 'pending': + return 'accent'; + case 'warning': + return 'warning'; + default: + return 'subdued'; + } +}; + +export const getRuleHealthColor = (rule: Rule) => { + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + if (isRuleLastRunOutcomeEnabled) { + return (rule.lastRun && getOutcomeHealthColor(rule.lastRun.outcome)) || 'subdued'; + } + return getExecutionStatusHealthColor(rule.executionStatus.status); +}; + +export const getIsLicenseError = (rule: Rule) => { + return rule.lastRun?.warning === RuleExecutionStatusErrorReasons.License; +}; + +export const getRuleStatusMessage = (rule: Rule) => { + const isLicenseError = getIsLicenseError(rule); + const isRuleLastRunOutcomeEnabled = getIsExperimentalFeatureEnabled('ruleLastRunOutcome'); + + if (isLicenseError) { + return ALERT_STATUS_LICENSE_ERROR; + } + if (isRuleLastRunOutcomeEnabled) { + return (rule.lastRun && rulesLastRunOutcomeTranslationMapping[rule.lastRun.outcome]) || ''; + } + return rulesStatusesTranslationsMapping[rule.executionStatus.status]; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index a0905a8645fbc9..415672324e648a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -40,6 +40,7 @@ import { RuleTypeParams, ActionVariable, RuleType as CommonRuleType, + RuleLastRun, } from '@kbn/alerting-plugin/common'; import type { BulkOperationError } from '@kbn/alerting-plugin/server'; import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; @@ -106,6 +107,7 @@ export type { RuleStatusDropdownProps, RuleTagFilterProps, RuleStatusFilterProps, + RuleLastRun, RuleTagBadgeProps, RuleTagBadgeOptions, RuleEventLogListProps, @@ -301,7 +303,7 @@ export interface RuleType< export type SanitizedRuleType = Omit; -export type RuleUpdates = Omit; +export type RuleUpdates = Omit; export interface RuleTableItem extends Rule { ruleType: RuleType['name']; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index ee111bfc9d0c66..47006f7a1a7d18 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -368,19 +368,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await refreshAlertsList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); - expect(refreshResults.map((item: any) => item.status).sort()).to.eql(['Error', 'Ok']); + expect(refreshResults.map((item: any) => item.status).sort()).to.eql([ + 'Failed', + 'Succeeded', + ]); }); await refreshAlertsList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await testSubjects.click('ruleExecutionStatusFilterButton'); - await testSubjects.click('ruleExecutionStatuserrorFilterOption'); // select Error status filter + await testSubjects.click('ruleLastRunOutcomeFilterButton'); + await testSubjects.click('ruleLastRunOutcomefailedFilterOption'); // select Error status filter await retry.try(async () => { const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); expect(filterErrorOnlyResults.length).to.equal(1); expect(filterErrorOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); expect(filterErrorOnlyResults[0].interval).to.equal('30 sec'); - expect(filterErrorOnlyResults[0].status).to.equal('Error'); + expect(filterErrorOnlyResults[0].status).to.equal('Failed'); expect(filterErrorOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/); }); }); @@ -393,7 +396,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(refreshResults.length).to.equal(1); expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); expect(refreshResults[0].interval).to.equal('1 min'); - expect(refreshResults[0].status).to.equal('Ok'); + expect(refreshResults[0].status).to.equal('Succeeded'); expect(refreshResults[0].duration).to.match(/\d{2,}:\d{2}/); }); @@ -417,11 +420,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { await refreshAlertsList(); expect(await testSubjects.getVisibleText('totalRulesCount')).to.be('2 rules'); - expect(await testSubjects.getVisibleText('totalActiveRulesCount')).to.be('Active: 0'); - expect(await testSubjects.getVisibleText('totalOkRulesCount')).to.be('Ok: 1'); - expect(await testSubjects.getVisibleText('totalErrorRulesCount')).to.be('Error: 1'); - expect(await testSubjects.getVisibleText('totalPendingRulesCount')).to.be('Pending: 0'); - expect(await testSubjects.getVisibleText('totalUnknownRulesCount')).to.be('Unknown: 0'); + expect(await testSubjects.getVisibleText('totalSucceededRulesCount')).to.be('Succeeded: 1'); + expect(await testSubjects.getVisibleText('totalFailedRulesCount')).to.be('Failed: 1'); + expect(await testSubjects.getVisibleText('totalWarningRulesCount')).to.be('Warning: 0'); }); }); @@ -433,7 +434,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(refreshResults.length).to.equal(1); expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); expect(refreshResults[0].interval).to.equal('1 min'); - expect(refreshResults[0].status).to.equal('Ok'); + expect(refreshResults[0].status).to.equal('Succeeded'); expect(refreshResults[0].duration).to.match(/\d{2,}:\d{2}/); }); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 6ca556876d0e7d..55cbf68ead3e04 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -93,6 +93,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'internalAlertsTable', 'ruleTagFilter', 'ruleStatusFilter', + 'ruleLastRunOutcome', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,