Skip to content

Commit

Permalink
[Actionable Observability] Add Alert Details page header (#140299)
Browse files Browse the repository at this point in the history
Co-authored-by: Faisal Kanout <faisal.kanout@elastic.co>
  • Loading branch information
CoenWarmer and fkanout authored Oct 4, 2022
1 parent d616210 commit c7a5660
Show file tree
Hide file tree
Showing 18 changed files with 599 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => {
}),
[caseData.comments]
);

const alertRegistrationContexts = useMemo(
() => getRegistrationContextFromAlerts(caseData.comments),
[caseData.comments]
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/cases/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ const uiMock: jest.Mocked<CasesUiStart['ui']> = {
getRecentCases: jest.fn(),
};

export const openAddToExistingCaseModalMock = jest.fn();

const hooksMock: jest.Mocked<CasesUiStart['hooks']> = {
getUseCasesAddToNewCaseFlyout: jest.fn(),
getUseCasesAddToExistingCaseModal: jest.fn(),
getUseCasesAddToExistingCaseModal: jest.fn().mockImplementation(() => ({
open: openAddToExistingCaseModalMock,
})),
};

const helpersMock: jest.Mocked<CasesUiStart['helpers']> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ interface AlertDetailParams {

export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => {
const { observabilityRuleTypeRegistry } = usePluginContext();

const params = useMemo(
() => ({ id, ruleType: observabilityRuleTypeRegistry }),
[id, observabilityRuleTypeRegistry]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,58 @@

import React from 'react';
import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting';
import { useParams } from 'react-router-dom';
import { Chance } from 'chance';
import { waitFor } from '@testing-library/react';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';

import { render } from '../../../utils/test_helper';
import { useKibana } from '../../../utils/kibana_react';
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail';
import { AlertDetails } from './alert_details';
import { Chance } from 'chance';
import { useParams } from 'react-router-dom';
import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs';
import { AlertDetails } from './alert_details';
import { ConfigSchema } from '../../../plugin';
import { alert, alertWithNoData } from '../mock/alert';
import { waitFor } from '@testing-library/react';

jest.mock('../../../hooks/use_fetch_alert_detail');
jest.mock('../../../hooks/use_breadcrumbs');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
}));

jest.mock('../../../utils/kibana_react');

const useKibanaMock = useKibana as jest.Mock;

const mockKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...kibanaStartMock.startContract(),
cases: casesPluginMock.createStartContract(),
http: {
basePath: {
prepend: jest.fn(),
},
},
triggersActionsUi: triggersActionsUiMock.createStart(),
},
});
};

jest.mock('../../../hooks/use_fetch_alert_detail');
jest.mock('../../../hooks/use_breadcrumbs');
jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: () => ({
all: true,
create: true,
delete: true,
push: true,
read: true,
update: true,
}),
}));

const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock;
const useParamsMock = useParams as jest.Mock;
const useBreadcrumbsMock = useBreadcrumbs as jest.Mock;
Expand All @@ -49,16 +84,20 @@ describe('Alert details', () => {
jest.clearAllMocks();
useParamsMock.mockReturnValue(params);
useBreadcrumbsMock.mockReturnValue([]);
mockKibana();
});

it('should show alert summary', async () => {
it('should show the alert detail page with all necessary components', async () => {
useFetchAlertDetailMock.mockReturnValue([false, alert]);

const alertDetails = render(<AlertDetails />, config);

expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy();
await waitFor(() => expect(alertDetails.queryByTestId('centerJustifiedSpinner')).toBeFalsy());

expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy();
expect(alertDetails.queryByTestId('alertDetailsError')).toBeFalsy();
expect(alertDetails.queryByTestId('page-title-container')).toBeTruthy();
expect(alertDetails.queryByTestId('alert-summary-container')).toBeTruthy();
});

it('should show error loading the alert details', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,36 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { useParams } from 'react-router-dom';
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';

import { useKibana } from '../../../utils/kibana_react';
import { ObservabilityAppServices } from '../../../application/types';
import { usePluginContext } from '../../../hooks/use_plugin_context';
import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs';
import { paths } from '../../../config/paths';
import { AlertDetailsPathParams } from '../types';
import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail';

import { AlertSummary, HeaderActions, PageTitle } from '.';
import { CenterJustifiedSpinner } from '../../rule_details/components/center_justified_spinner';
import { AlertSummary } from '.';
import PageNotFound from '../../404';
import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail';

import { ObservabilityAppServices } from '../../../application/types';
import { AlertDetailsPathParams } from '../types';
import { observabilityFeatureId } from '../../../../common';
import { paths } from '../../../config/paths';

export function AlertDetails() {
const { http } = useKibana<ObservabilityAppServices>().services;
const {
http,
cases: {
helpers: { canUseCases },
ui: { getCasesContext },
},
} = useKibana<ObservabilityAppServices>().services;
const { ObservabilityPageTemplate, config } = usePluginContext();
const { alertId } = useParams<AlertDetailsPathParams>();
const [isLoading, alert] = useFetchAlertDetail(alertId);

const CasesContext = getCasesContext();
const userCasesPermissions = canUseCases();

useBreadcrumbs([
{
href: http.basePath.prepend(paths.observability.alerts),
Expand Down Expand Up @@ -69,7 +82,22 @@ export function AlertDetails() {
);

return (
<ObservabilityPageTemplate data-test-subj="alertDetails">
<ObservabilityPageTemplate
pageHeader={{
pageTitle: <PageTitle title={alert?.reason} active={Boolean(alert?.active)} />,
rightSideItems: [
<CasesContext
owner={[observabilityFeatureId]}
permissions={userCasesPermissions}
features={{ alerts: { sync: false } }}
>
<HeaderActions alert={alert} />
</CasesContext>,
],
bottomBorder: false,
}}
data-test-subj="alertDetails"
>
<AlertSummary alert={alert} />
</ObservabilityPageTemplate>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function AlertSummary({ alert }: AlertSummaryProps) {
const tags = alert?.fields[ALERT_RULE_TAGS];

return (
<>
<div data-test-subj="alert-summary-container">
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xxs">
Expand Down Expand Up @@ -161,6 +161,6 @@ export function AlertSummary({ alert }: AlertSummaryProps) {
</div>
</EuiFlexItem>
</EuiFlexGroup>
</>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 { fireEvent } from '@testing-library/react';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
import { casesPluginMock, openAddToExistingCaseModalMock } from '@kbn/cases-plugin/public/mocks';

import { render } from '../../../utils/test_helper';
import { useKibana } from '../../../utils/kibana_react';
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
import { alertWithTags, mockAlertUuid } from '../mock/alert';

import { HeaderActions } from './header_actions';

jest.mock('../../../utils/kibana_react');

const useKibanaMock = useKibana as jest.Mock;

const mockKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...kibanaStartMock.startContract(),
triggersActionsUi: triggersActionsUiMock.createStart(),
cases: casesPluginMock.createStartContract(),
},
});
};

const ruleId = '123';
const ruleName = '456';

jest.mock('../../../hooks/use_fetch_rule', () => {
return {
useFetchRule: () => ({
reloadRule: jest.fn(),
rule: {
id: ruleId,
name: ruleName,
},
}),
};
});

describe('Header Actions', () => {
beforeEach(() => {
jest.clearAllMocks();
mockKibana();
});

it('should display an actions button', () => {
const { queryByTestId } = render(<HeaderActions alert={alertWithTags} />);
expect(queryByTestId('alert-details-header-actions-menu-button')).toBeTruthy();
});

describe('when clicking the actions button', () => {
it('should offer an "add to case" button which opens the add to case modal', async () => {
const { getByTestId, findByRole } = render(<HeaderActions alert={alertWithTags} />);

fireEvent.click(await findByRole('button', { name: 'Actions' }));

fireEvent.click(getByTestId('add-to-case-button'));

expect(openAddToExistingCaseModalMock).toBeCalledWith({
attachments: [
{
alertId: mockAlertUuid,
index: '.internal.alerts-observability.metrics.alerts-*',
rule: {
id: ruleId,
name: ruleName,
},
type: 'alert',
},
],
});
});
});
});
Loading

0 comments on commit c7a5660

Please sign in to comment.