Skip to content

Commit

Permalink
[Actionable Observability] o11y rules page (#127406)
Browse files Browse the repository at this point in the history
* style rules table, refactor useFetchRules hook, edit_rule_flyout component,delete, pagination, sorting

* remove unused import

* fix translations

* change name column to rule and statusFilter to lastResponse filter

* remove unused code

* embed create rule flyout & create loadRuleTypesHook that accepts a filteredSolutions param

* fix failing tests

* fix last failing test

* Show rule type name in the Rule column

* PR review comments

* useMemo for panelItems on status_context

* close status context popover when clicking outside of the context menu

* refactor useFetchRules to get rid of react-hooks/exhaustive-deps warning

* remove console log statement

* useCallback for EuiBasicTable onChange

* cleanup async fetchRuleTypes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
mgiota and kibanamachine authored Mar 16, 2022
1 parent 3dbaf8a commit ad5c67b
Show file tree
Hide file tree
Showing 20 changed files with 1,326 additions and 283 deletions.
62 changes: 62 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_fetch_rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { useEffect, useState, useCallback } from 'react';
import { loadRules, Rule } from '../../../triggers_actions_ui/public';
import { RULES_LOAD_ERROR } from '../pages/rules/translations';
import { FetchRulesProps } from '../pages/rules/types';
import { OBSERVABILITY_RULE_TYPES } from '../pages/rules/config';
import { useKibana } from '../utils/kibana_react';

interface RuleState {
isLoading: boolean;
data: Rule[];
error: string | null;
totalItemCount: number;
}

export function useFetchRules({ ruleLastResponseFilter, page, sort }: FetchRulesProps) {
const { http } = useKibana().services;

const [rulesState, setRulesState] = useState<RuleState>({
isLoading: false,
data: [],
error: null,
totalItemCount: 0,
});

const fetchRules = useCallback(async () => {
setRulesState((oldState) => ({ ...oldState, isLoading: true }));

try {
const response = await loadRules({
http,
page,
typesFilter: OBSERVABILITY_RULE_TYPES,
ruleStatusesFilter: ruleLastResponseFilter,
sort,
});
setRulesState((oldState) => ({
...oldState,
isLoading: false,
data: response.data,
totalItemCount: response.total,
}));
} catch (_e) {
setRulesState((oldState) => ({ ...oldState, isLoading: false, error: RULES_LOAD_ERROR }));
}
}, [http, page, ruleLastResponseFilter, sort]);
useEffect(() => {
fetchRules();
}, [fetchRules]);

return {
rulesState,
reload: fetchRules,
setRulesState,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 { EuiConfirmModal } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../../utils/kibana_react';
import {
confirmModalText,
confirmButtonText,
cancelButtonText,
deleteSuccessText,
deleteErrorText,
} from '../translations';

export function DeleteModalConfirmation({
idsToDelete,
apiDeleteCall,
onDeleted,
onCancel,
onErrors,
singleTitle,
multipleTitle,
setIsLoadingState,
}: {
idsToDelete: string[];
apiDeleteCall: ({
ids,
http,
}: {
ids: string[];
http: HttpSetup;
}) => Promise<{ successes: string[]; errors: string[] }>;
onDeleted: (deleted: string[]) => void;
onCancel: () => void;
onErrors: () => void;
singleTitle: string;
multipleTitle: string;
setIsLoadingState: (isLoading: boolean) => void;
}) {
const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState<boolean>(false);

useEffect(() => {
setDeleteModalVisibility(idsToDelete.length > 0);
}, [idsToDelete]);

const {
http,
notifications: { toasts },
} = useKibana().services;
const numIdsToDelete = idsToDelete.length;
if (!deleteModalFlyoutVisible) {
return null;
}

return (
<EuiConfirmModal
buttonColor="danger"
data-test-subj="deleteIdsConfirmation"
title={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)}
onCancel={() => {
setDeleteModalVisibility(false);
onCancel();
}}
onConfirm={async () => {
setDeleteModalVisibility(false);
setIsLoadingState(true);
const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http });
setIsLoadingState(false);

const numSuccesses = successes.length;
const numErrors = errors.length;
if (numSuccesses > 0) {
toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle));
}

if (numErrors > 0) {
toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle));
await onErrors();
}
await onDeleted(successes);
}}
cancelButtonText={cancelButtonText}
confirmButtonText={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)}
>
{confirmModalText(numIdsToDelete, singleTitle, multipleTitle)}
</EuiConfirmModal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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, useMemo, useEffect } from 'react';
import { useKibana } from '../../../utils/kibana_react';
import { EditFlyoutProps } from '../types';

export function EditRuleFlyout({ currentRule, onSave }: EditFlyoutProps) {
const { triggersActionsUi } = useKibana().services;
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);

useEffect(() => {
setEditFlyoutVisibility(true);
}, [currentRule]);
const EditAlertFlyout = useMemo(
() =>
triggersActionsUi.getEditAlertFlyout({
initialRule: currentRule,
onClose: () => {
setEditFlyoutVisibility(false);
},
onSave,
}),
[currentRule, setEditFlyoutVisibility, triggersActionsUi, onSave]
);
return <>{editFlyoutVisible && EditAlertFlyout}</>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { EuiHealth, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common';
import { getHealthColor, rulesStatusesTranslationsMapping } from '../config';
import { RULE_STATUS_LICENSE_ERROR } from '../translations';
import { ExecutionStatusProps } from '../types';

export function ExecutionStatus({ executionStatus }: ExecutionStatusProps) {
const healthColor = getHealthColor(executionStatus.status);
const tooltipMessage =
executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null;
const isLicenseError = executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License;
const statusMessage = isLicenseError
? RULE_STATUS_LICENSE_ERROR
: rulesStatusesTranslationsMapping[executionStatus.status];

const health = (
<EuiHealth data-test-subj={`ruleStatus-${executionStatus.status}`} color={healthColor}>
{statusMessage}
</EuiHealth>
);

const healthWithTooltip = tooltipMessage ? (
<EuiToolTip data-test-subj="ruleStatus-error-tooltip" position="top" content={tooltipMessage}>
{health}
</EuiToolTip>
) : (
health
);

return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>{healthWithTooltip}</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.
*/

/* eslint-disable react/function-component-definition */

import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFilterGroup,
EuiPopover,
EuiFilterButton,
EuiFilterSelectItem,
EuiHealth,
} from '@elastic/eui';
import { AlertExecutionStatuses, AlertExecutionStatusValues } from '../../../../../alerting/common';
import { getHealthColor, rulesStatusesTranslationsMapping } from '../config';
import { StatusFilterProps } from '../types';

export const LastResponseFilter: React.FunctionComponent<StatusFilterProps> = ({
selectedStatuses,
onChange,
}: StatusFilterProps) => {
const [selectedValues, setSelectedValues] = useState<string[]>(selectedStatuses);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);

useEffect(() => {
if (onChange) {
onChange(selectedValues);
}
}, [selectedValues, onChange]);

useEffect(() => {
setSelectedValues(selectedStatuses);
}, [selectedStatuses]);

return (
<EuiFilterGroup>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiFilterButton
iconType="arrowDown"
hasActiveFilters={selectedValues.length > 0}
numActiveFilters={selectedValues.length}
numFilters={selectedValues.length}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
data-test-subj="ruleStatusFilterButton"
>
<FormattedMessage
id="xpack.observability.rules.ruleLastResponseFilterLabel"
defaultMessage="Last response"
/>
</EuiFilterButton>
}
>
<div className="euiFilterSelect__items">
{[...AlertExecutionStatusValues].sort().map((item: AlertExecutionStatuses) => {
const healthColor = getHealthColor(item);
return (
<EuiFilterSelectItem
key={item}
style={{ textTransform: 'capitalize' }}
onClick={() => {
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={`ruleStatus${item}FilerOption`}
>
<EuiHealth color={healthColor}>{rulesStatusesTranslationsMapping[item]}</EuiHealth>
</EuiFilterSelectItem>
);
})}
</div>
</EuiPopover>
</EuiFilterGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import moment from 'moment';
import { LastRunProps } from '../types';

export function LastRun({ date }: LastRunProps) {
return (
<>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{moment(date).fromNow()}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiBadge } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { RuleNameProps } from '../types';
import { useKibana } from '../../../utils/kibana_react';

export function Name({ name, rule }: RuleNameProps) {
const { http } = useKibana().services;
const detailsLink = http.basePath.prepend(
`/app/management/insightsAndAlerting/triggersActions/rule/${rule.id}`
);
const link = (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiLink title={name} href={detailsLink}>
{name}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{rule.ruleType}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<>
{link}
{rule.enabled && rule.muteAll && (
<EuiBadge data-test-subj="mutedActionsBadge" color="hollow">
<FormattedMessage
id="xpack.observability.rules.rulesTable.columns.mutedBadge"
defaultMessage="Muted"
/>
</EuiBadge>
)}
</>
);
}
Loading

0 comments on commit ad5c67b

Please sign in to comment.