mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Alert Details] Add investigation guide empty state (#223974)
## Summary Resolves #222051. This PR makes a richer empty state UX for the Investigation Guide feature we added to the Alert Details page. Before, when a rule did not have an investigation guide, the tab on the alert details page dedicated to the feature was disabled. Now, the tab is always enabled. When an investigation guide is present on a rule, a badge will display to indicate the tab contains content. If the user clicks into the tab when the rule does not have an investigation guide, they will see an empty state with a call to action to create a guide. If the user decides to click the empty state button, it will open the Rule Edit flyout. I have added additional functionality that allows the flyout to take an `initialStep` prop, so we pre-set the flyout to the `Details` step which contains the text field the user can use to create their guide. The copy, iconography, and layout of the tab heading are all in draft state pending some design feedback. I will also add some tests to the code that I have added.  ## Reviewing this PR _Note to technical writers:_ You can see the copy added for the empty state [here](https://github.com/elastic/kibana/pull/223974/files#diff-71b439414e4974e2decb0f25c136f52ccea4b49ebe393af68dfc5fd184d56e1cR37). Here's a screenshot as well: <img width="375" alt="image" src="https://github.com/user-attachments/assets/491d87ac-b473-484e-82cd-45a1bd197c61" /> ### Technical review 1. Create a rule that will generate alerts; _do not_ define an Investigation Guide for it 1. Trigger an alert, and go to the details page 1. You should see the Investigation guide tab is available, whereas on `main` it would be disabled 1. Open the Investigation Guide tab, you should see the empty state with its CTA 1. Click this button, the rule flyout should open in Edit mode, and the Details step should be pre-selected 1. Define an investigation guide, this can be any text. Save the rule. 1. Once you have saved the rule you should see the rule data update in the page. The empty state will be gone and the investigation guide you defined will be there instead.
This commit is contained in:
parent
78b2013468
commit
dd2e7cb5f2
10 changed files with 359 additions and 56 deletions
|
@ -26,7 +26,7 @@ import {
|
|||
} from './rule_form_errors';
|
||||
import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations';
|
||||
import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils';
|
||||
import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';
|
||||
import { DEFAULT_VALID_CONSUMERS, RuleFormStepId, getDefaultFormData } from './constants';
|
||||
|
||||
export interface EditRuleFormProps {
|
||||
id: string;
|
||||
|
@ -38,6 +38,7 @@ export interface EditRuleFormProps {
|
|||
onSubmit?: (ruleId: string) => void;
|
||||
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
|
||||
initialMetadata?: RuleTypeMetaData;
|
||||
initialEditStep?: RuleFormStepId;
|
||||
}
|
||||
|
||||
export const EditRuleForm = (props: EditRuleFormProps) => {
|
||||
|
@ -51,6 +52,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
isFlyout,
|
||||
onChangeMetaData,
|
||||
initialMetadata,
|
||||
initialEditStep,
|
||||
} = props;
|
||||
const { http, notifications, docLinks, ruleTypeRegistry, application, fieldsMetadata, ...deps } =
|
||||
plugins;
|
||||
|
@ -229,6 +231,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
initialEditStep={initialEditStep}
|
||||
/>
|
||||
</RuleFormStateProvider>
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT,
|
||||
} from '../translations';
|
||||
import { RuleFormData } from '../types';
|
||||
import { RuleFormStepId } from '../constants';
|
||||
|
||||
jest.mock('../rule_definition', () => ({
|
||||
RuleDefinition: () => <div />,
|
||||
|
@ -118,6 +119,44 @@ describe('ruleFlyout', () => {
|
|||
expect(await screen.findByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('omitting `initialStep` causes default behavior with step 1 selected', () => {
|
||||
const { getByText } = render(<RuleFlyout onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
expect(getByText('Current step is 1'));
|
||||
expect(getByText('Step 2 is incomplete'));
|
||||
expect(getByText('Step 3 is incomplete'));
|
||||
});
|
||||
|
||||
test('setting `initialStep` to `RuleFormStepId.DEFINITION` will make step 1 the current step', () => {
|
||||
const { getByText } = render(
|
||||
<RuleFlyout onCancel={onCancel} onSave={onSave} initialEditStep={RuleFormStepId.DEFINITION} />
|
||||
);
|
||||
|
||||
expect(getByText('Current step is 1'));
|
||||
expect(getByText('Step 2 is incomplete'));
|
||||
expect(getByText('Step 3 is incomplete'));
|
||||
});
|
||||
|
||||
test('setting `initialStep` to `RuleFormStepId.ACTION` will make step 1 the current step', () => {
|
||||
const { getByText } = render(
|
||||
<RuleFlyout onCancel={onCancel} onSave={onSave} initialEditStep={RuleFormStepId.ACTIONS} />
|
||||
);
|
||||
|
||||
expect(getByText('Step 1 is complete'));
|
||||
expect(getByText('Current step is 2'));
|
||||
expect(getByText('Step 3 is incomplete'));
|
||||
});
|
||||
|
||||
test('setting `initialStep` to `RuleFormStepId.DETAILS` will make step 1 the current step', () => {
|
||||
const { getByText } = render(
|
||||
<RuleFlyout onCancel={onCancel} onSave={onSave} initialEditStep={RuleFormStepId.DETAILS} />
|
||||
);
|
||||
|
||||
expect(getByText('Step 1 is complete'));
|
||||
expect(getByText('Step 2 is incomplete'));
|
||||
expect(getByText('Current step is 3'));
|
||||
});
|
||||
|
||||
test('should call onSave when save button is pressed', async () => {
|
||||
render(<RuleFlyout onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ interface RuleFlyoutProps {
|
|||
onCancel?: () => void;
|
||||
onSave: (formData: RuleFormData) => void;
|
||||
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
|
||||
initialEditStep?: RuleFormStepId;
|
||||
}
|
||||
|
||||
// This component is only responsible for the CONTENT of the EuiFlyout. See `flyout/rule_form_flyout.tsx` for the
|
||||
|
@ -38,8 +39,9 @@ export const RuleFlyout = ({
|
|||
// we're displaying the confirmation modal for closing the flyout.
|
||||
onCancel: onClose = () => {},
|
||||
onChangeMetaData = () => {},
|
||||
initialEditStep,
|
||||
}: RuleFlyoutProps) => {
|
||||
const [initialStep, setInitialStep] = useState<RuleFormStepId | undefined>(undefined);
|
||||
const [initialStep, setInitialStep] = useState<RuleFormStepId | undefined>(initialEditStep);
|
||||
const [isConfirmCloseModalVisible, setIsConfirmCloseModalVisible] = useState(false);
|
||||
|
||||
const {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
RULE_FORM_ROUTE_PARAMS_ERROR_TITLE,
|
||||
} from './translations';
|
||||
import { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types';
|
||||
import { RuleFormStepId } from './constants';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
@ -41,6 +42,7 @@ export interface RuleFormProps<MetaData extends RuleTypeMetaData = RuleTypeMetaD
|
|||
showMustacheAutocompleteSwitch?: boolean;
|
||||
initialValues?: Partial<Omit<RuleFormData, 'ruleTypeId'>>;
|
||||
initialMetadata?: MetaData;
|
||||
initialEditStep?: RuleFormStepId;
|
||||
}
|
||||
|
||||
export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
||||
|
@ -65,6 +67,7 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
showMustacheAutocompleteSwitch,
|
||||
initialValues,
|
||||
initialMetadata,
|
||||
initialEditStep,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -122,6 +125,7 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch}
|
||||
connectorFeatureId={connectorFeatureId}
|
||||
initialMetadata={initialMetadata}
|
||||
initialEditStep={initialEditStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -175,25 +179,26 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
docLinks,
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
fieldsMetadata,
|
||||
contentManagement,
|
||||
onChangeMetaData,
|
||||
id,
|
||||
ruleTypeId,
|
||||
validConsumers,
|
||||
multiConsumerSelection,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onChangeMetaData,
|
||||
isFlyout,
|
||||
showMustacheAutocompleteSwitch,
|
||||
connectorFeatureId,
|
||||
initialMetadata,
|
||||
initialEditStep,
|
||||
consumer,
|
||||
multiConsumerSelection,
|
||||
hideInterval,
|
||||
validConsumers,
|
||||
filteredRuleTypes,
|
||||
shouldUseRuleProducer,
|
||||
canShowConsumerSelection,
|
||||
initialValues,
|
||||
fieldsMetadata,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,8 +18,8 @@ import {
|
|||
EuiTabbedContentTab,
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiMarkdownFormat,
|
||||
EuiNotificationBadge,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AlertStatus,
|
||||
|
@ -39,6 +39,7 @@ import { usePageReady } from '@kbn/ebt-tools';
|
|||
import { RelatedAlerts } from './components/related_alerts/related_alerts';
|
||||
import { AlertDetailsSource } from './types';
|
||||
import { SourceBar } from './components';
|
||||
import { InvestigationGuide } from './components/investigation_guide';
|
||||
import { StatusBar } from './components/status_bar';
|
||||
import { observabilityFeatureId } from '../../../common';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
@ -119,7 +120,7 @@ export function AlertDetails() {
|
|||
const userCasesPermissions = canUseCases([observabilityFeatureId]);
|
||||
const ruleId = alertDetail?.formatted.fields[ALERT_RULE_UUID];
|
||||
const { rule, refetch } = useFetchRule({
|
||||
ruleId,
|
||||
ruleId: ruleId || '',
|
||||
});
|
||||
|
||||
const onSuccessAddSuggestedDashboard = useCallback(async () => {
|
||||
|
@ -322,24 +323,26 @@ export function AlertDetails() {
|
|||
{
|
||||
id: 'investigation_guide',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.observability.alertDetails.tab.investigationGuideLabel"
|
||||
defaultMessage="Investigation guide"
|
||||
/>
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.alertDetails.tab.investigationGuideLabel"
|
||||
defaultMessage="Investigation guide"
|
||||
/>
|
||||
{rule?.artifacts?.investigation_guide?.blob && (
|
||||
<EuiNotificationBadge color="success" css={{ marginLeft: '5px' }}>
|
||||
<EuiIcon type="dot" size="s" />
|
||||
</EuiNotificationBadge>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
'data-test-subj': 'investigationGuideTab',
|
||||
disabled: !rule?.artifacts?.investigation_guide?.blob,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiMarkdownFormat
|
||||
css={css`
|
||||
word-wrap: break-word;
|
||||
`}
|
||||
>
|
||||
{rule?.artifacts?.investigation_guide?.blob ?? ''}
|
||||
</EuiMarkdownFormat>
|
||||
</>
|
||||
<InvestigationGuide
|
||||
blob={rule?.artifacts?.investigation_guide?.blob}
|
||||
onUpdate={onUpdate}
|
||||
refetch={refetch}
|
||||
rule={rule}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -401,6 +404,8 @@ export function AlertDetails() {
|
|||
alertStatus={alertStatus}
|
||||
onUntrackAlert={onUntrackAlert}
|
||||
onUpdate={onUpdate}
|
||||
rule={rule}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</CasesContext>,
|
||||
],
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
|
||||
import { RuleFormStepId } from '@kbn/response-ops-rule-form/src/constants';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface AlertDetailsRuleFormFlyoutBaseProps {
|
||||
onUpdate?: () => void;
|
||||
refetch: () => void;
|
||||
rule?: Rule;
|
||||
}
|
||||
|
||||
interface Props extends AlertDetailsRuleFormFlyoutBaseProps {
|
||||
initialEditStep?: RuleFormStepId;
|
||||
isRuleFormFlyoutOpen: boolean;
|
||||
setIsRuleFormFlyoutOpen: React.Dispatch<boolean>;
|
||||
rule: Rule;
|
||||
}
|
||||
|
||||
export function AlertDetailsRuleFormFlyout({
|
||||
initialEditStep,
|
||||
onUpdate,
|
||||
refetch,
|
||||
isRuleFormFlyoutOpen,
|
||||
setIsRuleFormFlyoutOpen,
|
||||
rule,
|
||||
}: Props) {
|
||||
const { services } = useKibana();
|
||||
if (!isRuleFormFlyoutOpen) return null;
|
||||
const {
|
||||
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
|
||||
} = services;
|
||||
return (
|
||||
<RuleFormFlyout
|
||||
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
|
||||
id={rule.id}
|
||||
onCancel={() => {
|
||||
setIsRuleFormFlyoutOpen(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onUpdate?.();
|
||||
refetch();
|
||||
setIsRuleFormFlyoutOpen(false);
|
||||
}}
|
||||
initialEditStep={initialEditStep}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -104,6 +104,12 @@ describe('Header Actions', () => {
|
|||
alertIndex={'alert-index'}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -129,6 +135,12 @@ describe('Header Actions', () => {
|
|||
alertIndex={'alert-index'}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -141,6 +153,7 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(queryByTestId('alert-details-header-actions-menu-button')).toBeTruthy();
|
||||
|
@ -153,6 +166,12 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -166,6 +185,12 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -180,6 +205,12 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -193,6 +224,12 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
// @ts-expect-error partial implementation for testing
|
||||
rule={{
|
||||
id: mockRuleId,
|
||||
name: mockRuleName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -218,6 +255,7 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -231,6 +269,7 @@ describe('Header Actions', () => {
|
|||
alert={untrackedAlert}
|
||||
alertStatus={untrackedAlert.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -244,6 +283,7 @@ describe('Header Actions', () => {
|
|||
alert={alertWithGroupsAndTags}
|
||||
alertStatus={alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus}
|
||||
onUntrackAlert={mockOnUntrackAlert}
|
||||
refetch={jest.fn()}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(await findByTestId('alert-details-header-actions-menu-button'));
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { noop } from 'lodash';
|
||||
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
|
||||
import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public/types';
|
||||
import { AttachmentType } from '@kbn/cases-plugin/common';
|
||||
import {
|
||||
|
@ -29,17 +28,19 @@ import {
|
|||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useFetchRule } from '../../../hooks/use_fetch_rule';
|
||||
import type { TopAlert } from '../../../typings/alerts';
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
import { useBulkUntrackAlerts } from '../hooks/use_bulk_untrack_alerts';
|
||||
import {
|
||||
AlertDetailsRuleFormFlyout,
|
||||
type AlertDetailsRuleFormFlyoutBaseProps,
|
||||
} from './AlertDetailsRuleFormFlyout';
|
||||
|
||||
export interface HeaderActionsProps {
|
||||
export interface HeaderActionsProps extends AlertDetailsRuleFormFlyoutBaseProps {
|
||||
alert: TopAlert | null;
|
||||
alertIndex?: string;
|
||||
alertStatus?: AlertStatus;
|
||||
onUntrackAlert: () => void;
|
||||
onUpdate?: () => void;
|
||||
}
|
||||
|
||||
export function HeaderActions({
|
||||
|
@ -48,26 +49,19 @@ export function HeaderActions({
|
|||
alertStatus,
|
||||
onUntrackAlert,
|
||||
onUpdate,
|
||||
rule,
|
||||
refetch,
|
||||
}: HeaderActionsProps) {
|
||||
const { services } = useKibana();
|
||||
const {
|
||||
cases: {
|
||||
hooks: { useCasesAddToExistingCaseModal },
|
||||
},
|
||||
triggersActionsUi: {
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
getRuleSnoozeModal: RuleSnoozeModal,
|
||||
},
|
||||
triggersActionsUi: { getRuleSnoozeModal: RuleSnoozeModal },
|
||||
http,
|
||||
} = services;
|
||||
|
||||
const { rule, refetch } = useFetchRule({
|
||||
ruleId: alert?.fields[ALERT_RULE_UUID] || '',
|
||||
});
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState<boolean>(false);
|
||||
const [snoozeModalOpen, setSnoozeModalOpen] = useState<boolean>(false);
|
||||
|
||||
const selectCaseModal = useCasesAddToExistingCaseModal();
|
||||
|
@ -84,6 +78,8 @@ export function HeaderActions({
|
|||
}
|
||||
}, [alert, untrackAlerts, onUntrackAlert]);
|
||||
|
||||
const [alertDetailsRuleFormFlyoutOpen, setAlertDetailsRuleFormFlyoutOpen] = useState(false);
|
||||
|
||||
const handleTogglePopover = () => setIsPopoverOpen(!isPopoverOpen);
|
||||
const handleClosePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
|
@ -107,11 +103,6 @@ export function HeaderActions({
|
|||
selectCaseModal.open({ getAttachments: () => attachments });
|
||||
};
|
||||
|
||||
const handleEditRuleDetails = () => {
|
||||
setIsPopoverOpen(false);
|
||||
setRuleConditionsFlyoutOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenSnoozeModal = () => {
|
||||
setIsPopoverOpen(false);
|
||||
setSnoozeModalOpen(true);
|
||||
|
@ -175,7 +166,10 @@ export function HeaderActions({
|
|||
size="s"
|
||||
color="text"
|
||||
iconType="pencil"
|
||||
onClick={handleEditRuleDetails}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setAlertDetailsRuleFormFlyoutOpen(true);
|
||||
}}
|
||||
disabled={!alert?.fields[ALERT_RULE_UUID] || !rule}
|
||||
data-test-subj="edit-rule-button"
|
||||
>
|
||||
|
@ -225,20 +219,15 @@ export function HeaderActions({
|
|||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{rule && ruleConditionsFlyoutOpen ? (
|
||||
<RuleFormFlyout
|
||||
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
|
||||
id={rule.id}
|
||||
onCancel={() => {
|
||||
setRuleConditionsFlyoutOpen(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setRuleConditionsFlyoutOpen(false);
|
||||
onUpdate?.();
|
||||
refetch();
|
||||
}}
|
||||
{rule && (
|
||||
<AlertDetailsRuleFormFlyout
|
||||
isRuleFormFlyoutOpen={alertDetailsRuleFormFlyoutOpen}
|
||||
setIsRuleFormFlyoutOpen={setAlertDetailsRuleFormFlyoutOpen}
|
||||
onUpdate={onUpdate}
|
||||
refetch={refetch}
|
||||
rule={rule}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{rule && snoozeModalOpen ? (
|
||||
<RuleSnoozeModal
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { InvestigationGuide } from './investigation_guide';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import * as kibana from '../../../utils/kibana_react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
jest.mock('@kbn/response-ops-rule-form/flyout', () => {
|
||||
return {
|
||||
// we mock the response-ops flyout because we aren't testing it here
|
||||
RuleFormFlyout: () => <div>Mock Flyout</div>,
|
||||
};
|
||||
});
|
||||
|
||||
describe('InvestigationGuide', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(kibana, 'useKibana').mockReturnValue({
|
||||
services: {
|
||||
triggersActionsUi: {
|
||||
// @ts-expect-error partial implementation for mocking
|
||||
ruleTypeRegistry: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
// @ts-expect-error partial implementation for mocking
|
||||
actionTypeRegistry: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('provides an empty state that will open the rule form flyout', async () => {
|
||||
const mockRule = { id: 'mock' };
|
||||
const { getByRole, getByText } = render(
|
||||
<InvestigationGuide
|
||||
onUpdate={() => {}}
|
||||
refetch={() => {}}
|
||||
// @ts-expect-error internal hook call is mocked, do not need real values
|
||||
rule={mockRule}
|
||||
/>
|
||||
);
|
||||
|
||||
// grab add guide button for functionality testing
|
||||
const addGuideButton = getByRole('button', { name: 'Add guide' });
|
||||
expect(addGuideButton).toBeInTheDocument();
|
||||
|
||||
// verify that clicking the add guide button opens the flyout
|
||||
await act(() => fireEvent.click(addGuideButton));
|
||||
|
||||
expect(getByText('Mock Flyout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the investigation guide when one is provided', async () => {
|
||||
// provide actual markdown and test it's getting rendered properly
|
||||
const mockMarkdown =
|
||||
'## This is an investigation guide\n\nCall **The team** to resolve _any issues_.\n';
|
||||
const mockRule = { id: 'mock' };
|
||||
|
||||
const { getByRole } = render(
|
||||
<InvestigationGuide
|
||||
onUpdate={() => {}}
|
||||
refetch={() => {}}
|
||||
// @ts-expect-error internal hook call is mocked, do not need real values
|
||||
rule={mockRule}
|
||||
blob={mockMarkdown}
|
||||
/>
|
||||
);
|
||||
|
||||
// test that the component is rendering markdown
|
||||
expect(getByRole('heading', { name: 'This is an investigation guide' }));
|
||||
expect(getByRole('strong')).toHaveTextContent('The team');
|
||||
expect(getByRole('emphasis')).toHaveTextContent('any issues');
|
||||
});
|
||||
});
|
|
@ -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 { EuiEmptyPrompt, EuiButton, EuiSpacer, EuiMarkdownFormat } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { RuleFormStepId } from '@kbn/response-ops-rule-form/src/constants';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
AlertDetailsRuleFormFlyout,
|
||||
type AlertDetailsRuleFormFlyoutBaseProps,
|
||||
} from './AlertDetailsRuleFormFlyout';
|
||||
|
||||
interface InvestigationGuideProps extends AlertDetailsRuleFormFlyoutBaseProps {
|
||||
blob?: string;
|
||||
}
|
||||
|
||||
export function InvestigationGuide({ blob, onUpdate, refetch, rule }: InvestigationGuideProps) {
|
||||
const [alertDetailsRuleFormFlyoutOpen, setAlertDetailsRuleFormFlyoutOpen] = useState(false);
|
||||
return blob ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiMarkdownFormat
|
||||
css={css`
|
||||
word-wrap: break-word;
|
||||
`}
|
||||
>
|
||||
{blob}
|
||||
</EuiMarkdownFormat>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiEmptyPrompt
|
||||
iconType="logoObservability"
|
||||
iconColor="default"
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.alertDetails.investigationGide.emptyPrompt.title"
|
||||
defaultMessage="Add an Investigation Guide"
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.alertDetails.investigationGide.emptyPrompt.body"
|
||||
defaultMessage="Add a guide to your alert's rule."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="xpack.observability.alertDetails.investigationGuide.emptyPrompt.addGuide"
|
||||
color="primary"
|
||||
onClick={() => setAlertDetailsRuleFormFlyoutOpen(true)}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.alertDetails.investigationGide.emptyPrompt.addGuideButton.copy"
|
||||
defaultMessage="Add guide"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
{!!rule && (
|
||||
<AlertDetailsRuleFormFlyout
|
||||
initialEditStep={RuleFormStepId.DETAILS}
|
||||
isRuleFormFlyoutOpen={alertDetailsRuleFormFlyoutOpen}
|
||||
setIsRuleFormFlyoutOpen={setAlertDetailsRuleFormFlyoutOpen}
|
||||
onUpdate={onUpdate}
|
||||
refetch={refetch}
|
||||
rule={rule}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue