-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Alerting V2] Episode table actions #260195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cfbe35b
2088e18
483575b
c9c9e6d
f1fcb01
38357d4
9af9e6a
cf1b96f
fefa77e
3c4a7b1
f061998
25cc2da
280cd5e
486863b
9d64b59
bb9c735
cf5410f
6516e49
3edf707
208b28e
c64e731
e28a580
c8f69bb
73971b3
177b8be
d893731
58baef0
624620d
2b630e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /* | ||
| * 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 { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import type { HttpStart } from '@kbn/core-http-browser'; | ||
| import { httpServiceMock } from '@kbn/core-http-browser-mocks'; | ||
| import { ALERT_EPISODE_ACTION_TYPE } from '@kbn/alerting-v2-schemas'; | ||
| import { AcknowledgeActionButton } from './acknowledge_action_button'; | ||
| import { useCreateAlertAction } from '../../../hooks/use_create_alert_action'; | ||
|
|
||
| jest.mock('../../../hooks/use_create_alert_action'); | ||
|
|
||
| const useCreateAlertActionMock = jest.mocked(useCreateAlertAction); | ||
|
|
||
| const mockHttp: HttpStart = httpServiceMock.createStartContract(); | ||
|
|
||
| describe('AcknowledgeActionButton', () => { | ||
| const mutate = jest.fn(); | ||
| beforeEach(() => { | ||
| mutate.mockReset(); | ||
| useCreateAlertActionMock.mockReturnValue({ | ||
| mutate, | ||
| isLoading: false, | ||
| } as unknown as ReturnType<typeof useCreateAlertAction>); | ||
| }); | ||
|
|
||
| it('renders Acknowledge when lastAckAction is undefined (same as not acknowledged)', () => { | ||
| render(<AcknowledgeActionButton http={mockHttp} />); | ||
| expect(screen.getByText('Acknowledge')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders Unacknowledge when lastAckAction is ack', () => { | ||
| render( | ||
| <AcknowledgeActionButton lastAckAction={ALERT_EPISODE_ACTION_TYPE.ACK} http={mockHttp} /> | ||
| ); | ||
| expect(screen.getByText('Unacknowledge')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders Acknowledge when lastAckAction is unack', () => { | ||
| render( | ||
| <AcknowledgeActionButton lastAckAction={ALERT_EPISODE_ACTION_TYPE.UNACK} http={mockHttp} /> | ||
| ); | ||
| expect(screen.getByText('Acknowledge')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('calls ack route mutation on click', async () => { | ||
| const user = userEvent.setup(); | ||
| render( | ||
| <AcknowledgeActionButton | ||
| lastAckAction={ALERT_EPISODE_ACTION_TYPE.UNACK} | ||
| episodeId="ep-1" | ||
| groupHash="gh-1" | ||
| http={mockHttp} | ||
| /> | ||
| ); | ||
|
|
||
| await user.click(screen.getByTestId('alertEpisodeAcknowledgeActionButton')); | ||
|
|
||
| expect(mutate).toHaveBeenCalledWith({ | ||
| groupHash: 'gh-1', | ||
| actionType: ALERT_EPISODE_ACTION_TYPE.ACK, | ||
| body: { episode_id: 'ep-1' }, | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| * 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, { useCallback } from 'react'; | ||
| import { EuiButton } from '@elastic/eui'; | ||
| import { i18n } from '@kbn/i18n'; | ||
| import type { HttpStart } from '@kbn/core-http-browser'; | ||
| import { ALERT_EPISODE_ACTION_TYPE } from '@kbn/alerting-v2-schemas'; | ||
| import { useCreateAlertAction } from '../../../hooks/use_create_alert_action'; | ||
|
|
||
| export interface AcknowledgeActionButtonProps { | ||
| lastAckAction?: string | null; | ||
| episodeId?: string; | ||
| groupHash?: string | null; | ||
| http: HttpStart; | ||
| } | ||
|
|
||
| export function AcknowledgeActionButton({ | ||
| lastAckAction, | ||
| episodeId, | ||
| groupHash, | ||
| http, | ||
| }: AcknowledgeActionButtonProps) { | ||
| const isAcknowledged = lastAckAction === ALERT_EPISODE_ACTION_TYPE.ACK; | ||
| const actionType = isAcknowledged | ||
| ? ALERT_EPISODE_ACTION_TYPE.UNACK | ||
| : ALERT_EPISODE_ACTION_TYPE.ACK; | ||
| const { mutate: createAlertAction, isLoading } = useCreateAlertAction(http); | ||
|
|
||
| const label = isAcknowledged | ||
| ? i18n.translate('xpack.alertingV2.episodesUi.acknowledgeAction.unacknowledge', { | ||
|
adcoelho marked this conversation as resolved.
|
||
| defaultMessage: 'Unacknowledge', | ||
| }) | ||
| : i18n.translate('xpack.alertingV2.episodesUi.acknowledgeAction.acknowledge', { | ||
| defaultMessage: 'Acknowledge', | ||
| }); | ||
|
|
||
| const handleClick = useCallback(() => { | ||
| if (!episodeId || !groupHash) { | ||
| return; | ||
| } | ||
| createAlertAction({ | ||
| groupHash, | ||
| actionType, | ||
| body: { episode_id: episodeId }, | ||
| }); | ||
| }, [createAlertAction, episodeId, groupHash, actionType]); | ||
|
|
||
| return ( | ||
| <EuiButton | ||
| size="s" | ||
| color="text" | ||
| fill={false} | ||
| iconType={isAcknowledged ? 'crossCircle' : 'checkCircle'} | ||
| onClick={handleClick} | ||
| isLoading={isLoading} | ||
| isDisabled={!episodeId || !groupHash} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering if we should hide the button instead by returning |
||
| data-test-subj="alertEpisodeAcknowledgeActionButton" | ||
| > | ||
| {label} | ||
| </EuiButton> | ||
| ); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it misses some tests like opening the popover, etc. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| * 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 { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import type { HttpStart } from '@kbn/core-http-browser'; | ||
| import { httpServiceMock } from '@kbn/core-http-browser-mocks'; | ||
| import { ALERT_EPISODE_ACTION_TYPE } from '@kbn/alerting-v2-schemas'; | ||
| import { AlertEpisodeActionsCell } from './alert_episode_actions_cell'; | ||
| import { useCreateAlertAction } from '../../../hooks/use_create_alert_action'; | ||
|
|
||
| jest.mock('../../../hooks/use_create_alert_action'); | ||
|
|
||
| const useCreateAlertActionMock = jest.mocked(useCreateAlertAction); | ||
|
|
||
| const mockHttp: HttpStart = httpServiceMock.createStartContract(); | ||
|
|
||
| describe('AlertEpisodeActionsCell', () => { | ||
| beforeEach(() => { | ||
| useCreateAlertActionMock.mockReturnValue({ | ||
| mutate: jest.fn(), | ||
| isLoading: false, | ||
| } as unknown as ReturnType<typeof useCreateAlertAction>); | ||
| }); | ||
|
|
||
| it('renders acknowledge, snooze, and more-actions controls', () => { | ||
| render(<AlertEpisodeActionsCell http={mockHttp} />); | ||
| expect(screen.getByTestId('alertEpisodeAcknowledgeActionButton')).toBeInTheDocument(); | ||
| expect(screen.getByTestId('alertEpisodeSnoozeActionButton')).toBeInTheDocument(); | ||
| expect(screen.getByTestId('alertingEpisodeActionsMoreButton')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('opens popover and shows Resolve when not deactivated', async () => { | ||
| const user = userEvent.setup(); | ||
| render( | ||
| <AlertEpisodeActionsCell | ||
| http={mockHttp} | ||
| groupAction={{ | ||
| groupHash: 'g1', | ||
| ruleId: 'r1', | ||
| lastDeactivateAction: null, | ||
| lastSnoozeAction: null, | ||
| snoozeExpiry: null, | ||
| tags: [], | ||
| }} | ||
| /> | ||
| ); | ||
| await user.click(screen.getByTestId('alertingEpisodeActionsMoreButton')); | ||
| expect(screen.getByText('Resolve')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('shows Unresolve in popover when group action is deactivated', async () => { | ||
| const user = userEvent.setup(); | ||
| render( | ||
| <AlertEpisodeActionsCell | ||
| http={mockHttp} | ||
| groupAction={{ | ||
| groupHash: 'g1', | ||
| ruleId: 'r1', | ||
| lastDeactivateAction: ALERT_EPISODE_ACTION_TYPE.DEACTIVATE, | ||
| lastSnoozeAction: null, | ||
| snoozeExpiry: null, | ||
| tags: [], | ||
| }} | ||
| /> | ||
| ); | ||
| await user.click(screen.getByTestId('alertingEpisodeActionsMoreButton')); | ||
| expect(screen.getByText('Unresolve')).toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import React, { useState } from 'react'; | ||
| import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiPopover } from '@elastic/eui'; | ||
| import { i18n } from '@kbn/i18n'; | ||
| import type { HttpStart } from '@kbn/core-http-browser'; | ||
| import { AcknowledgeActionButton } from './acknowledge_action_button'; | ||
| import { SnoozeActionButton } from './snooze_action_button'; | ||
| import type { EpisodeAction, GroupAction } from '../../../types/action'; | ||
| import { ResolveActionButton } from './resolve_action_button'; | ||
|
|
||
| export interface AlertEpisodeActionsCellProps { | ||
| episodeId?: string; | ||
| groupHash?: string; | ||
|
adcoelho marked this conversation as resolved.
|
||
| episodeAction?: EpisodeAction; | ||
| groupAction?: GroupAction; | ||
| http: HttpStart; | ||
| } | ||
|
|
||
| export function AlertEpisodeActionsCell({ | ||
| episodeId, | ||
| groupHash, | ||
| episodeAction, | ||
| groupAction, | ||
| http, | ||
| }: AlertEpisodeActionsCellProps) { | ||
| const [isMoreOpen, setIsMoreOpen] = useState(false); | ||
|
|
||
| return ( | ||
| <EuiFlexGroup | ||
| gutterSize="xs" | ||
| wrap | ||
| responsive={true} | ||
| alignItems="center" | ||
| justifyContent="flexEnd" | ||
| > | ||
| <EuiFlexItem grow={false}> | ||
| <AcknowledgeActionButton | ||
| lastAckAction={episodeAction?.lastAckAction} | ||
| episodeId={episodeId} | ||
| groupHash={groupHash} | ||
| http={http} | ||
| /> | ||
| </EuiFlexItem> | ||
| <EuiFlexItem grow={false}> | ||
| <SnoozeActionButton | ||
| lastSnoozeAction={groupAction?.lastSnoozeAction} | ||
| groupHash={groupHash} | ||
| http={http} | ||
| /> | ||
| </EuiFlexItem> | ||
| <EuiFlexItem grow={false}> | ||
| <EuiPopover | ||
| aria-label={i18n.translate( | ||
| 'xpack.alertingV2.episodesUi.actionsCell.moreActionsAriaLabel', | ||
| { | ||
| defaultMessage: 'More actions', | ||
| } | ||
| )} | ||
| button={ | ||
| <EuiButtonIcon | ||
| display="empty" | ||
| color="text" | ||
| size="xs" | ||
| iconType="boxesHorizontal" | ||
| aria-label={i18n.translate( | ||
| 'xpack.alertingV2.episodesUi.actionsCell.moreActionsAriaLabel', | ||
| { | ||
| defaultMessage: 'More actions', | ||
| } | ||
| )} | ||
| onClick={() => setIsMoreOpen((open) => !open)} | ||
| data-test-subj="alertingEpisodeActionsMoreButton" | ||
| /> | ||
| } | ||
| isOpen={isMoreOpen} | ||
| closePopover={() => setIsMoreOpen(false)} | ||
| anchorPosition="downLeft" | ||
| panelPaddingSize="s" | ||
| > | ||
| <EuiListGroup gutterSize="none" bordered={false} flush={true} size="l"> | ||
| <ResolveActionButton | ||
| lastDeactivateAction={groupAction?.lastDeactivateAction} | ||
| groupHash={groupHash} | ||
| http={http} | ||
| /> | ||
| </EuiListGroup> | ||
| </EuiPopover> | ||
| </EuiFlexItem> | ||
| </EuiFlexGroup> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * 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 userEvent from '@testing-library/user-event'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { AlertEpisodeSnoozeForm, computeEpisodeSnoozedUntil } from './alert_episode_snooze_form'; | ||
|
|
||
| describe('AlertEpisodeSnoozeForm', () => { | ||
| it('computeEpisodeSnoozedUntil returns a future ISO date', () => { | ||
| const before = Date.now(); | ||
| const result = computeEpisodeSnoozedUntil(1, 'h'); | ||
| const after = Date.now(); | ||
| const parsed = Date.parse(result); | ||
|
|
||
| expect(Number.isNaN(parsed)).toBe(false); | ||
| expect(parsed).toBeGreaterThanOrEqual(before + 3_600_000); | ||
| expect(parsed).toBeLessThanOrEqual(after + 3_600_000 + 1_000); | ||
| }); | ||
|
|
||
| it('applies preset snooze duration when a preset is clicked', async () => { | ||
| const user = userEvent.setup(); | ||
| const onApplySnooze = jest.fn(); | ||
|
|
||
| render(<AlertEpisodeSnoozeForm onApplySnooze={onApplySnooze} />); | ||
|
|
||
| await user.click(screen.getByRole('button', { name: '1 hour' })); | ||
|
|
||
| expect(onApplySnooze).toHaveBeenCalledTimes(1); | ||
| expect(Number.isNaN(Date.parse(onApplySnooze.mock.calls[0][0]))).toBe(false); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.