Skip to content

Commit

Permalink
added detectors view/edit ux (#40)
Browse files Browse the repository at this point in the history
Signed-off-by: Amardeepsingh Siglani <[email protected]>

Signed-off-by: Amardeepsingh Siglani <[email protected]>
(cherry picked from commit c2858ff)
  • Loading branch information
amsiglan authored and github-actions[bot] committed Nov 3, 2022
1 parent 06bd81f commit d7b2d04
Show file tree
Hide file tree
Showing 14 changed files with 1,772 additions and 127 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiAccordion,
EuiHorizontalRule,
EuiLink,
EuiResizableContainer,
EuiResizablePanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { AlertCondition, Detector } from '../../../../../models/interfaces';
import React, { useEffect } from 'react';
import { createTextDetailsGroup } from '../../../../utils/helpers';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { DEFAULT_EMPTY_DATA, getNotificationDetailsHref } from '../../../../utils/constants';
import { FeatureChannelList, RuleInfo } from '../../../../../server/models/interfaces';

export interface AlertTriggerViewProps {
alertTrigger: AlertCondition;
orderPosition: number;
detector: Detector;
notificationChannels: FeatureChannelList[];
rules: { [key: string]: RuleInfo };
}

export const AlertTriggerView: React.FC<AlertTriggerViewProps> = ({
alertTrigger,
orderPosition,
detector,
notificationChannels,
rules,
}) => {
const { name, sev_levels, types, tags, ids, severity, actions } = alertTrigger;
const alertSeverity = parseAlertSeverityToOption(severity)?.label || DEFAULT_EMPTY_DATA;
const action = actions[0];
const notificationChannelId = detector.triggers[orderPosition].actions[0].destination_id;
const notificationChannel = notificationChannels.find(
(channel) => channel.config_id === notificationChannelId
);
const conditionRuleNames = ids.map((ruleId) => rules[ruleId]?._source.title);
return (
<div>
{orderPosition > 0 && <EuiHorizontalRule />}
<EuiSpacer size={'l'} />
<EuiAccordion
id={`alert-trigger-${orderPosition}`}
paddingSize={'m'}
initialIsOpen={false}
buttonContent={
<EuiText size="m">
<p>{`Alert on ${name}`}</p>
</EuiText>
}
>
<EuiSpacer size="m" />
{createTextDetailsGroup([{ label: 'Trigger name', content: `${name}` }])}
<EuiSpacer size="xl" />

<EuiTitle size="s">
<h5>If any detection rule matches</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
{createTextDetailsGroup(
[
{ label: 'Log type', content: `${types[0]}` || DEFAULT_EMPTY_DATA },
{ label: 'Rule names', content: conditionRuleNames.join('\n') || DEFAULT_EMPTY_DATA },
],
3
)}
{createTextDetailsGroup(
[
{ label: 'Rule severities', content: sev_levels.join('\n') || DEFAULT_EMPTY_DATA },
{ label: 'Tags', content: tags.join('\n') || DEFAULT_EMPTY_DATA },
],
3
)}
<EuiSpacer size="xl" />

<EuiTitle size="s">
<h5>Alert and notify</h5>
</EuiTitle>
<EuiSpacer size="m" />
{createTextDetailsGroup([
{
label: 'Trigger alerts with severity',
content: `${alertSeverity}` || DEFAULT_EMPTY_DATA,
},
])}
<EuiSpacer size="l" />
{createTextDetailsGroup([
{
label: 'Notify channel',
content: notificationChannel ? (
<EuiLink href={getNotificationDetailsHref(action.destination_id)} target={'_blank'}>
{notificationChannel.name}
</EuiLink>
) : (
<EuiText>{DEFAULT_EMPTY_DATA}</EuiText>
),
},
])}
</EuiAccordion>
<EuiSpacer size={'m'} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiButton, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { ContentPanel } from '../../../../components/ContentPanel';
import { createTextDetailsGroup, parseSchedule } from '../../../../utils/helpers';
import moment from 'moment';
import { Detector } from '../../../../../models/interfaces';
import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants';

export interface DetectorBasicDetailsViewProps {
detector: Detector;
rulesCanFold?: boolean;
enabled_time?: number;
last_update_time?: number;
onEditClicked: () => void;
}

export const DetectorBasicDetailsView: React.FC<DetectorBasicDetailsViewProps> = ({
detector,
enabled_time,
last_update_time,
rulesCanFold,
children,
onEditClicked,
}) => {
const { name, detector_type, inputs, schedule } = detector;
const detectorSchedule = parseSchedule(schedule);
const createdAt = enabled_time ? moment(enabled_time).format('YYYY-MM-DDTHH:mm') : undefined;
const lastUpdated = last_update_time
? moment(last_update_time).format('YYYY-MM-DDTHH:mm')
: undefined;

return (
<ContentPanel
title={'Detector details'}
actions={[<EuiButton onClick={onEditClicked}>Edit</EuiButton>]}
>
<EuiSpacer size={'l'} />
{createTextDetailsGroup(
[
{ label: 'Detector name', content: name },
{ label: 'Log type', content: detector_type.toLowerCase() },
{ label: 'Data source', content: inputs[0].detector_input.indices[0] },
],
4
)}
{createTextDetailsGroup(
[
{ label: 'Description', content: inputs[0].detector_input.description },
{ label: 'Detector schedule', content: detectorSchedule },
{ label: 'Created at', content: createdAt || DEFAULT_EMPTY_DATA },
{ label: 'Last updated time', content: lastUpdated || DEFAULT_EMPTY_DATA },
],
4
)}
{rulesCanFold ? children : null}
</ContentPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { ContentPanel } from '../../../../components/ContentPanel';
import React, { useContext, useEffect, useState } from 'react';
import { EuiAccordion, EuiButton, EuiInMemoryTable, EuiSpacer, EuiText } from '@elastic/eui';
import {
RuleItem,
RuleItemInfo,
} from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces';
import { getRulesColumns } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/utils/constants';
import { ServicesContext } from '../../../../services';
import { ruleItemInfosToItems } from '../../../../utils/helpers';
import { Detector } from '../../../../../models/interfaces';
import { RuleInfo } from '../../../../../server/models/interfaces/Rules';

export interface DetectorRulesViewProps {
detector: Detector;
rulesCanFold?: boolean;
onEditClicked: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void;
}

export const DetectorRulesView: React.FC<DetectorRulesViewProps> = (props) => {
const totalSelected = props.detector.inputs.reduce((sum, inputObj) => {
return (
sum +
inputObj.detector_input.custom_rules.length +
inputObj.detector_input.pre_packaged_rules.length
);
}, 0);

const [enabledRuleItems, setEnabledRuleItems] = useState<RuleItem[]>([]);
const [allRuleItems, setAllRuleItems] = useState<RuleItem[]>([]);
const actions = [
<EuiButton onClick={() => props.onEditClicked(enabledRuleItems, allRuleItems)}>Edit</EuiButton>,
];
const services = useContext(ServicesContext);

useEffect(() => {
const getRules = async (prePackaged: boolean): Promise<RuleInfo[]> => {
const getRulesRes = await services?.ruleService.getRules(prePackaged, {
from: 0,
size: 5000,
query: {
nested: {
path: 'rule',
query: {
bool: {
must: [
{ match: { 'rule.category': `${props.detector.detector_type.toLowerCase()}` } },
],
},
},
},
},
});

if (getRulesRes?.ok) {
return getRulesRes.response.hits.hits;
}

return [];
};

const translateToRuleItems = (
prePackagedRules: RuleInfo[],
customRules: RuleInfo[],
isEnabled: (rule: RuleInfo) => boolean
) => {
let ruleItemInfos: RuleItemInfo[] = prePackagedRules.map((rule) => ({
...rule,
enabled: isEnabled(rule),
prePackaged: true,
}));

ruleItemInfos = ruleItemInfos.concat(
customRules.map((rule) => ({
...rule,
enabled: isEnabled(rule),
prePackaged: false,
}))
);

return ruleItemInfosToItems(props.detector.detector_type, ruleItemInfos);
};

const updateRulesState = async () => {
const enabledPrePackagedRuleIds = new Set(
props.detector.inputs[0].detector_input.pre_packaged_rules.map((ruleInfo) => ruleInfo.id)
);
const enabledCustomRuleIds = new Set(
props.detector.inputs[0].detector_input.custom_rules.map((ruleInfo) => ruleInfo.id)
);

const prePackagedRules = await getRules(true);
const customRules = await getRules(false);

const enabledPrePackagedRules = prePackagedRules.filter((hit: RuleInfo) => {
return enabledPrePackagedRuleIds.has(hit._id);
});

const enabledCustomRules = customRules.filter((hit: RuleInfo) => {
return enabledCustomRuleIds.has(hit._id);
});

const enabledRuleItems = translateToRuleItems(
enabledPrePackagedRules,
enabledCustomRules,
() => true
);
const allRuleItems = translateToRuleItems(
prePackagedRules,
customRules,
(ruleInfo) =>
enabledPrePackagedRuleIds.has(ruleInfo._id) || enabledCustomRuleIds.has(ruleInfo._id)
);
setEnabledRuleItems(enabledRuleItems);
setAllRuleItems(allRuleItems);
};

updateRulesState().catch((error) => {
// TODO: Show error toast
});
}, [services, props.detector]);

const rules = (
<EuiInMemoryTable
columns={getRulesColumns(false)}
items={enabledRuleItems}
itemId={(item: RuleItem) => `${item.name}`}
pagination
/>
);

return props.rulesCanFold ? (
<EuiAccordion
id={props.detector.name}
title="View detection rules"
buttonContent={
<EuiText size="m">
<p>View detection rules</p>
</EuiText>
}
>
<EuiSpacer size="l" />
{rules}
</EuiAccordion>
) : (
<ContentPanel title={`Detection rules (${totalSelected})`} actions={actions}>
{rules}
</ContentPanel>
);
};
Loading

0 comments on commit d7b2d04

Please sign in to comment.