Skip to content

Commit

Permalink
[Actionable Observability] Add rule details page (#130330)
Browse files Browse the repository at this point in the history
* Add rule details page

* Fix route

* Fix route

* Add useBreadcrumbs

* Add rule summary

* Complete rule data summary

* Update styling

* Add update rule

* Add edit role

* Update desgin

* Add conditions

* Add connectors icons

* Fix more button

* Remove unused FelxBox

* Add fetch alerts

* Move to items to components folder

* Format dates

* Add tabs

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Use the shared getRuleStatusDropdown

* Add permissions

* Better handling errors

* Fix styling

* Fix tag style

* Add tags component

* Use tags component from triggers aciton ui

* Add last24hAlerts hook

* Fix last 24h alerts count hook

* Fix large font

* Fix font size

* Fix size Actions

* Fix fontsize page header

* Fix conditions size

* Fix text move vertically on small screen

* Update style

* Update alerts counts style

* Cleanup

* Add formatter for the interval

* Add edit button on the definition section

* Add delete modal

* Add loader

* Fix conditions panctuation

* Fix size

* Use the healthColor function from rule component

* Add loading while deleting a rule

* Use connectors name to show actions

* Fix type

* Fix rule page

* Fix types

* Use common RULES_PAGE_LINK var

* Fix checks

* Better error handling

* Better i18n

* Code review

* Fix checks i18n

* Use abort signal

* Revert signal for loadRule as there is no tests

* Fix style

* Fixing tests

* Reduce bundle size

* Fix i18n

* Bump limits
  • Loading branch information
fkanout authored May 16, 2022
1 parent ffc515b commit 0248e93
Show file tree
Hide file tree
Showing 18 changed files with 1,110 additions and 4 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pageLoadAssetSize:
telemetry: 51957
telemetryManagementSection: 38586
transform: 41007
triggersActionsUi: 104400
triggersActionsUi: 105800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728
upgradeAssistant: 81241
urlForwarding: 32579
usageCollection: 39762
Expand Down
159 changes: 159 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_fetch_last24h_alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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.
*/
/*
* 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, useRef } from 'react';
import { AsApiContract } from '@kbn/actions-plugin/common';
import { HttpSetup } from '@kbn/core/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants';
import { RULE_LOAD_ERROR } from '../pages/rule_details/translations';

interface UseFetchLast24hAlertsProps {
http: HttpSetup;
features: string;
ruleId: string;
}
interface FetchLast24hAlerts {
isLoadingLast24hAlerts: boolean;
last24hAlerts: number;
errorLast24hAlerts: string | undefined;
}

export function useFetchLast24hAlerts({ http, features, ruleId }: UseFetchLast24hAlertsProps) {
const [last24hAlerts, setLast24hAlerts] = useState<FetchLast24hAlerts>({
isLoadingLast24hAlerts: true,
last24hAlerts: 0,
errorLast24hAlerts: undefined,
});
const isCancelledRef = useRef(false);
const abortCtrlRef = useRef(new AbortController());
const fetchLast24hAlerts = useCallback(async () => {
isCancelledRef.current = false;
abortCtrlRef.current.abort();
abortCtrlRef.current = new AbortController();
try {
if (!features) return;
const { index } = await fetchIndexNameAPI({
http,
features,
});
const { error, alertsCount } = await fetchLast24hAlertsAPI({
http,
index,
ruleId,
signal: abortCtrlRef.current.signal,
});
if (error) throw error;
if (!isCancelledRef.current) {
setLast24hAlerts((oldState: FetchLast24hAlerts) => ({
...oldState,
last24hAlerts: alertsCount,
isLoading: false,
}));
}
} catch (error) {
if (!isCancelledRef.current) {
if (error.name !== 'AbortError') {
setLast24hAlerts((oldState: FetchLast24hAlerts) => ({
...oldState,
isLoading: false,
errorLast24hAlerts: RULE_LOAD_ERROR(
error instanceof Error ? error.message : typeof error === 'string' ? error : ''
),
}));
}
}
}
}, [http, features, ruleId]);
useEffect(() => {
fetchLast24hAlerts();
}, [fetchLast24hAlerts]);

return last24hAlerts;
}

interface IndexName {
index: string;
}

export async function fetchIndexNameAPI({
http,
features,
}: {
http: HttpSetup;
features: string;
}): Promise<IndexName> {
const res = await http.get<{ index_name: string[] }>(`${BASE_RAC_ALERTS_API_PATH}/index`, {
query: { features },
});
return {
index: res.index_name[0],
};
}
export async function fetchLast24hAlertsAPI({
http,
index,
ruleId,
signal,
}: {
http: HttpSetup;
index: string;
ruleId: string;
signal: AbortSignal;
}): Promise<{
error: string | null;
alertsCount: number;
}> {
try {
const res = await http.post<AsApiContract<any>>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
signal,
body: JSON.stringify({
index,
query: {
bool: {
must: [
{
term: {
'kibana.alert.rule.uuid': ruleId,
},
},
{
range: {
'@timestamp': {
gte: 'now-24h',
lt: 'now',
},
},
},
],
},
},
aggs: {
alerts_count: {
cardinality: {
field: 'kibana.alert.uuid',
},
},
},
}),
});
return {
error: null,
alertsCount: res.aggregations.alerts_count.value,
};
} catch (error) {
return {
error,
alertsCount: 0,
};
}
}
46 changes: 46 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_fetch_rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { loadRule } from '@kbn/triggers-actions-ui-plugin/public';
import { FetchRuleProps, FetchRule } from '../pages/rule_details/types';
import { RULE_LOAD_ERROR } from '../pages/rule_details/translations';

export function useFetchRule({ ruleId, http }: FetchRuleProps) {
const [ruleSummary, setRuleSummary] = useState<FetchRule>({
isRuleLoading: true,
rule: undefined,
errorRule: undefined,
});
const fetchRuleSummary = useCallback(async () => {
try {
const rule = await loadRule({
http,
ruleId,
});

setRuleSummary((oldState: FetchRule) => ({
...oldState,
isRuleLoading: false,
rule,
}));
} catch (error) {
setRuleSummary((oldState: FetchRule) => ({
...oldState,
isRuleLoading: false,
errorRule: RULE_LOAD_ERROR(
error instanceof Error ? error.message : typeof error === 'string' ? error : ''
),
}));
}
}, [ruleId, http]);
useEffect(() => {
fetchRuleSummary();
}, [fetchRuleSummary]);

return { ...ruleSummary, reloadRule: fetchRuleSummary };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { ActionConnector, loadAllActions } from '@kbn/triggers-actions-ui-plugin/public';
import { FetchRuleActionsProps } from '../pages/rule_details/types';
import { ACTIONS_LOAD_ERROR } from '../pages/rule_details/translations';

interface FetchActions {
isLoadingActions: boolean;
allActions: Array<ActionConnector<Record<string, unknown>>>;
errorActions: string | undefined;
}

export function useFetchRuleActions({ http }: FetchRuleActionsProps) {
const [ruleActions, setRuleActions] = useState<FetchActions>({
isLoadingActions: true,
allActions: [] as Array<ActionConnector<Record<string, unknown>>>,
errorActions: undefined,
});

const fetchRuleActions = useCallback(async () => {
try {
const response = await loadAllActions({
http,
});
setRuleActions((oldState: FetchActions) => ({
...oldState,
isLoadingActions: false,
allActions: response,
}));
} catch (error) {
setRuleActions((oldState: FetchActions) => ({
...oldState,
isLoadingActions: false,
errorActions: ACTIONS_LOAD_ERROR(
error instanceof Error ? error.message : typeof error === 'string' ? error : ''
),
}));
}
}, [http]);
useEffect(() => {
fetchRuleActions();
}, [fetchRuleActions]);

return { ...ruleActions, reloadRuleActions: fetchRuleActions };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { loadRuleSummary } from '@kbn/triggers-actions-ui-plugin/public';
import { FetchRuleSummaryProps, FetchRuleSummary } from '../pages/rule_details/types';
import { RULE_LOAD_ERROR } from '../pages/rule_details/translations';

export function useFetchRuleSummary({ ruleId, http }: FetchRuleSummaryProps) {
const [ruleSummary, setRuleSummary] = useState<FetchRuleSummary>({
isLoadingRuleSummary: true,
ruleSummary: undefined,
errorRuleSummary: undefined,
});

const fetchRuleSummary = useCallback(async () => {
setRuleSummary((oldState: FetchRuleSummary) => ({ ...oldState, isLoading: true }));

try {
const response = await loadRuleSummary({
http,
ruleId,
});
setRuleSummary((oldState: FetchRuleSummary) => ({
...oldState,
isLoading: false,
ruleSummary: response,
}));
} catch (error) {
setRuleSummary((oldState: FetchRuleSummary) => ({
...oldState,
isLoading: false,
errorRuleSummary: RULE_LOAD_ERROR(
error instanceof Error ? error.message : typeof error === 'string' ? error : ''
),
}));
}
}, [ruleId, http]);
useEffect(() => {
fetchRuleSummary();
}, [fetchRuleSummary]);

return { ...ruleSummary, reloadRuleSummary: fetchRuleSummary };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 {
EuiText,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
IconType,
EuiLoadingSpinner,
} from '@elastic/eui';
import { intersectionBy } from 'lodash';
import { ActionsProps } from '../types';
import { useFetchRuleActions } from '../../../hooks/use_fetch_rule_actions';
import { useKibana } from '../../../utils/kibana_react';

interface MapActionTypeIcon {
[key: string]: string | IconType;
}
const mapActionTypeIcon: MapActionTypeIcon = {
/* TODO: Add the rest of the application logs (SVGs ones) */
'.server-log': 'logsApp',
'.email': 'email',
'.pagerduty': 'apps',
'.index': 'indexOpen',
'.slack': 'logoSlack',
'.webhook': 'logoWebhook',
};
export function Actions({ ruleActions }: ActionsProps) {
const {
http,
notifications: { toasts },
} = useKibana().services;
const { isLoadingActions, allActions, errorActions } = useFetchRuleActions({ http });
if (ruleActions && ruleActions.length <= 0) return <EuiText size="s">0</EuiText>;
const actions = intersectionBy(allActions, ruleActions, 'actionTypeId');
if (isLoadingActions) return <EuiLoadingSpinner size="s" />;
return (
<EuiFlexGroup direction="column">
{actions.map((action) => (
<>
<EuiFlexGroup alignItems="baseline">
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={mapActionTypeIcon[action.actionTypeId] ?? 'apps'} />
</EuiFlexItem>
<EuiFlexItem style={{ margin: '0px' }}>
<EuiText size="s">{action.name}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
))}
{errorActions && toasts.addDanger({ title: errorActions })}
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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.
*/

export { PageTitle } from './page_title';
export { ItemTitleRuleSummary } from './item_title_rule_summary';
export { ItemValueRuleSummary } from './item_value_rule_summary';
export { Actions } from './actions';
Loading

0 comments on commit 0248e93

Please sign in to comment.