diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index bf6f0ef43b820..ad727566957de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -133,6 +133,7 @@ export const AlertEdit = ({ aria-labelledby="flyoutAlertEditTitle" size="m" maxWidth={620} + ownFocus={false} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx index 38343116825dd..aca111df97e34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx @@ -95,13 +95,13 @@ export const AlertStatusFilter: React.FunctionComponent export function getHealthColor(status: AlertExecutionStatuses) { switch (status) { case 'active': - return 'primary'; + return 'success'; case 'error': return 'danger'; case 'ok': return 'subdued'; case 'pending': - return 'success'; + return 'accent'; default: return 'warning'; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss index fda7d6aa0b622..f45f010228501 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss @@ -5,3 +5,29 @@ color: $euiColorDarkShade; } } + +.alertSidebarItem { + &:hover, + &:focus-within, + &[class*='-isActive'] { + .alertSidebarItem__action { + opacity: 1; + } + } +} + +/** + * 1. Only visually hide the action, so that it's still accessible to screen readers. + * 2. When tabbed to, this element needs to be visible for keyboard accessibility. + */ +.alertSidebarItem__action { + opacity: 0; /* 1 */ + + &.alertSidebarItem__mobile { + opacity: 1; + } + + &:focus { + opacity: 1; /* 2 */ + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 16a2b4fae8cbf..cf41fe344919e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -31,15 +31,17 @@ import { EuiTableSortingType, EuiSwitch, EuiIcon, + EuiButtonIcon, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { isEmpty } from 'lodash'; +import moment from 'moment'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; -import { AlertAdd } from '../../alert_form'; +import { AlertAdd, AlertEdit } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; -import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; +import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions_new'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { AlertStatusFilter, getHealthColor } from './alert_status_filter'; @@ -50,6 +52,8 @@ import { disableAlert, enableAlert, deleteAlerts, + unmuteAlert, + muteAlert, } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasExecuteActionsCapability } from '../../../lib/capabilities'; @@ -107,6 +111,9 @@ export const AlertsList: React.FunctionComponent = () => { const [alertStatusesFilter, setAlertStatusesFilter] = useState([]); const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); const [dismissAlertErrors, setDismissAlertErrors] = useState(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [sort, setSort] = useState['sort']>({ field: 'name', direction: 'asc', @@ -136,6 +143,10 @@ export const AlertsList: React.FunctionComponent = () => { totalItemCount: 0, }); const [alertsToDelete, setAlertsToDelete] = useState([]); + const onRuleEdit = (ruleItem: AlertTableItem) => { + setEditFlyoutVisibility(true); + setCurrentRuleToEdit(ruleItem); + }; useEffect(() => { loadAlertsData(); @@ -174,15 +185,14 @@ export const AlertsList: React.FunctionComponent = () => { (async () => { try { const result = await loadActionTypes({ http }); - setActionTypes( - result - .filter( - // TODO: Remove "DEFAULT_HIDDEN_ACTION_TYPES" when cases connector is available across Kibana. - // Issue: https://github.com/elastic/kibana/issues/82502. - ({ id }) => actionTypeRegistry.has(id) && !DEFAULT_HIDDEN_ACTION_TYPES.includes(id) - ) - .sort((a, b) => a.name.localeCompare(b.name)) - ); + const sortedResult = result + .filter( + // TODO: Remove "DEFAULT_HIDDEN_ACTION_TYPES" when cases connector is available across Kibana. + // Issue: https://github.com/elastic/kibana/issues/82502. + ({ id }) => actionTypeRegistry.has(id) && !DEFAULT_HIDDEN_ACTION_TYPES.includes(id) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + setActionTypes(sortedResult); } catch (e) { toasts.addDanger({ title: i18n.translate( @@ -325,31 +335,34 @@ export const AlertsList: React.FunctionComponent = () => { const alertsTableColumns = [ { name: '', - width: '100px', + width: '120px', render(item: AlertTableItem) { + let isEnabled = !!item.enabled; return ( { - const enabled = !item.enabled; + const enabled = item.enabled; asyncScheduler.schedule(async () => { if (enabled) { await disableAlert({ http, id: item.id }); + isEnabled = false; } else { await enableAlert({ http, id: item.id }); + isEnabled = true; } loadAlertsData(); }, 10); - // setIsDisabled(!item.enabled); }} label={!item.enabled ? disabledLabel : enabledLabel} /> ); }, + 'data-test-subj': 'alertsTableCell-enabled', }, { field: 'name', @@ -359,7 +372,7 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: true, truncateText: true, - width: '35%', + width: '30%', 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { const ruleType = alertTypesState.data.get(alert.alertTypeId); @@ -399,12 +412,33 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: true, truncateText: false, - width: '150px', + width: '120px', 'data-test-subj': 'alertsTableCell-status', - render: (executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { return renderAlertExecutionStatus(item.executionStatus, item); }, }, + { + field: 'lastUpdated', + width: '100px', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.lastUpdatedTitle', + { defaultMessage: 'Last updated' } + ), + render: (_count: number, item: AlertTableItem) => { + const today = moment(Date.now()); + const updatedAt = moment(item.updatedAt); + const lastUpdatedMin = today.diff(updatedAt, 'minutes'); + const lastUpdatedSec = today.diff(updatedAt, 'seconds'); + return ( + + {lastUpdatedMin === 0 ? `${lastUpdatedSec}s ago` : `${lastUpdatedMin}m ago`} + + ); + }, + sortable: true, + 'data-test-subj': 'alertsTableCell-lastUpdated', + }, { field: 'alertType', name: i18n.translate( @@ -413,6 +447,9 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: false, truncateText: true, + render: (_count: number, item: AlertTableItem) => ( + {item.alertType} + ), 'data-test-subj': 'alertsTableCell-alertType', }, { @@ -423,64 +460,166 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: false, 'data-test-subj': 'alertsTableCell-tagsText', + render: (_count: number, item: AlertTableItem) => ( +
+ {item.tagsText} +
+ ), }, { - field: 'schedule', + field: 'schedule.interval', + width: '15%', name: i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle', { defaultMessage: 'Schedule' } ), - render: (count: number, item: AlertTableItem) => { + render: (interval: number, item: AlertTableItem) => { return ( - <> - - - - ); - }, - sortable: false, - 'data-test-subj': 'alertsTableCell-actionsCount', - }, - { - field: 'actionsCount', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount', - { defaultMessage: 'Actions' } - ), - render: (count: number, item: AlertTableItem) => { - return ( - - {count} - + + + + +   + {interval} + + + +
+ + +   + {item.actionsCount} + +
+
+ + {item.muteAll ? ( +
+ + } + > + { + asyncScheduler.schedule(async () => { + await unmuteAlert({ http, id: item.id }); + loadAlertsData(); + }, 10); + }} + iconType={'eyeClosed'} + aria-label={'unmute'} + /> + +
+ ) : ( +
+ + } + > + { + asyncScheduler.schedule(async () => { + await muteAlert({ http, id: item.id }); + loadAlertsData(); + }, 10); + }} + iconType={'eye'} + aria-label={'mute'} + /> + +
+ )} +
+
); }, sortable: false, - 'data-test-subj': 'alertsTableCell-actionsCount', - }, - { - field: 'schedule.interval', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle', - { defaultMessage: 'Runs every' } - ), - sortable: false, truncateText: false, 'data-test-subj': 'alertsTableCell-interval', }, { name: '', - width: '40px', + width: '10%', render(item: AlertTableItem) { return ( - loadAlertsData()} - setAlertsToDelete={setAlertsToDelete} - /> + + + + + + } + > + onRuleEdit(item)} + iconType={'pencil'} + aria-label={'mute'} + /> + + + + + } + > + { + asyncScheduler.schedule(async () => { + await muteAlert({ http, id: item.id }); + loadAlertsData(); + }, 10); + }} + iconType={'trash'} + aria-label={'mute'} + /> + + + + + + loadAlertsData()} + setAlertsToDelete={setAlertsToDelete} + onEditAlert={() => onRuleEdit(item)} + /> + + ); }, }, @@ -683,7 +822,7 @@ export const AlertsList: React.FunctionComponent = () => { - + { - + { onSave={loadAlertsData} /> )} + {editFlyoutVisible && currentRuleToEdit && ( + { + setEditFlyoutVisibility(false); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + onSave={loadAlertsData} + /> + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss index 774d1566f2a63..0a75ff243ad20 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss @@ -8,7 +8,7 @@ padding: $euiSizeM; } -.actCollapsedItemActions__delete { +.actCollapsedItemActions { display: flex; .actCollapsedItemActions__deleteIcon { @@ -16,7 +16,7 @@ text-align: center; } - .actCollapsedItemActions__deleteLabel { + .actCollapsedItemActions__Label { padding-left: $euiSizeS; padding-top: $euiSizeXS * .5; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions_new.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions_new.tsx new file mode 100644 index 0000000000000..dec1f7b90c340 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions_new.tsx @@ -0,0 +1,148 @@ +/* + * 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 { asyncScheduler } from 'rxjs'; +import React, { useEffect, useState } from 'react'; +import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; + +import { AlertTableItem } from '../../../../types'; +import { + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import './collapsed_item_actions.scss'; + +export type ComponentOpts = { + item: AlertTableItem; + onAlertChanged: () => void; + setAlertsToDelete: React.Dispatch>; + onEditAlert: (item: AlertTableItem) => void; +} & BulkOperationsComponentOpts; + +export const CollapsedItemActions: React.FunctionComponent = ({ + item, + onAlertChanged, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + setAlertsToDelete, + onEditAlert, +}: ComponentOpts) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isDisabled, setIsDisabled] = useState(!item.enabled); + const [isMuted, setIsMuted] = useState(item.muteAll); + useEffect(() => { + setIsDisabled(!item.enabled); + setIsMuted(item.muteAll); + }, [item.enabled, item.muteAll]); + + const button = ( + setIsPopoverOpen(!isPopoverOpen)} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle', + { defaultMessage: 'Actions' } + )} + /> + ); + + const panels = [ + { + id: 0, + hasFocus: false, + items: [ + { + disabled: !(item.isEditable && !isDisabled) || !item.enabledInLicense, + 'data-test-subj': 'muteSwitch', + onClick: async () => { + const muteAll = isMuted; + asyncScheduler.schedule(async () => { + if (muteAll) { + await unmuteAlert({ ...item, muteAll }); + } else { + await muteAlert({ ...item, muteAll }); + } + onAlertChanged(); + }, 10); + setIsMuted(!isMuted); + }, + name: isMuted + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.unmuteTitle', + { defaultMessage: 'Unmute' } + ) + : i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle', + { defaultMessage: 'Mute' } + ), + }, + { + disabled: !item.isEditable || !item.enabledInLicense, + 'data-test-subj': 'disableSwitch', + onClick: async () => { + const enabled = !isDisabled; + asyncScheduler.schedule(async () => { + if (enabled) { + await disableAlert({ ...item, enabled }); + } else { + await enableAlert({ ...item, enabled }); + } + onAlertChanged(); + }, 10); + setIsDisabled(!isDisabled); + }, + name: isDisabled + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle', + { defaultMessage: 'Enable' } + ) + : i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle', + { defaultMessage: 'Disable' } + ), + }, + { + disabled: !item.isEditable, + 'data-test-subj': 'editAlert', + onClick: () => onEditAlert(item), + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.editTitle', + { defaultMessage: 'Edit rule' } + ), + }, + { + disabled: !item.isEditable, + 'data-test-subj': 'deleteAlert', + onClick: () => setAlertsToDelete([item.id]), + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle', + { defaultMessage: 'Delete rule' } + ), + }, + ], + }, + ]; + + return ( + setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="none" + data-test-subj="collapsedItemActions" + > + + + ); +}; + +export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions);