Skip to content

Commit

Permalink
[ResponseOps][Connectors] Pager duty connector UI (elastic#171748)
Browse files Browse the repository at this point in the history
Fixes elastic#170048

## Summary

This PR adds support in the `UI` for the `custom_details` and links
attributes in the Pagerduty connector.

### Release Notes

PagerDuty connector now supports the links and custom_details
attributes.
  • Loading branch information
adcoelho authored Nov 27, 2023
1 parent 75e8f3d commit 903149a
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { screen, render } from '@testing-library/react';
import { LinksList } from './links_list';
import userEvent from '@testing-library/user-event';

describe('LinksList', () => {
const editAction = jest.fn();

const options = {
index: 0,
errors: {
links: [],
},
editAction,
links: [],
};

beforeEach(() => jest.clearAllMocks());

it('the list is empty by default', () => {
render(<LinksList {...options} />);

expect(screen.queryByTestId('linksListItemRow')).not.toBeInTheDocument();
});

it('clicking add button calls editAction with correct params', async () => {
render(<LinksList {...options} />);

userEvent.click(await screen.findByTestId('pagerDutyAddLinkButton'));

expect(editAction).toHaveBeenCalledWith('links', [{ href: '', text: '' }], 0);
});

it('clicking remove link button calls editAction with correct params', async () => {
render(
<LinksList
{...options}
links={[
{ href: '1', text: 'foobar' },
{ href: '2', text: 'foobar' },
{ href: '3', text: 'foobar' },
]}
/>
);

expect(await screen.findAllByTestId('linksListItemRow', { exact: false })).toHaveLength(3);

userEvent.click((await screen.findAllByTestId('pagerDutyRemoveLinkButton'))[1]);

expect(editAction).toHaveBeenCalledWith(
'links',
[
{ href: '1', text: 'foobar' },
{ href: '3', text: 'foobar' },
],
0
);
});

it('editing a link href field calls editAction with correct params', async () => {
render(<LinksList {...options} links={[{ href: '', text: 'foobar' }]} />);

expect(await screen.findByTestId('linksListItemRow', { exact: false })).toBeInTheDocument();

userEvent.paste(await screen.findByTestId('linksHrefInput'), 'newHref');

expect(editAction).toHaveBeenCalledWith('links', [{ href: 'newHref', text: 'foobar' }], 0);
});

it('editing a link text field calls editAction with correct params', async () => {
render(<LinksList {...options} links={[{ href: 'foobar', text: '' }]} />);

expect(await screen.findByTestId('linksListItemRow', { exact: false })).toBeInTheDocument();

userEvent.paste(await screen.findByTestId('linksTextInput'), 'newText');

expect(editAction).toHaveBeenCalledWith('links', [{ href: 'foobar', text: 'newText' }], 0);
});

it('correctly displays error messages', async () => {
render(<LinksList {...options} errors={{ links: ['FoobarError'] }} />);

expect(await screen.findByText('FoobarError'));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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 {
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ActionParamsProps,
TextFieldWithMessageVariables,
} from '@kbn/triggers-actions-ui-plugin/public';
import { PagerDutyActionParams } from '../types';

type LinksListProps = Pick<
ActionParamsProps<PagerDutyActionParams>,
'index' | 'editAction' | 'errors' | 'messageVariables'
> &
Pick<PagerDutyActionParams, 'links'>;

export const LinksList: React.FC<LinksListProps> = ({
editAction,
errors,
index,
links,
messageVariables,
}) => {
const areLinksInvalid = Array.isArray(errors.links) && errors.links.length > 0;

return (
<EuiFormRow
id="pagerDutyLinks"
label={i18n.translate('xpack.stackConnectors.components.pagerDuty.linksFieldLabel', {
defaultMessage: 'Links (optional)',
})}
isInvalid={areLinksInvalid}
error={errors.links}
fullWidth
>
<EuiFlexGroup direction="column" data-test-subj="linksList" gutterSize="s">
{links &&
links.map((link, currentLinkIndex) => (
<EuiFlexItem data-test-subj={`linksListItemRow-${currentLinkIndex}`}>
<EuiSpacer size="s" />
<EuiFlexGroup key={`linksListItemRow-${currentLinkIndex}`}>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.stackConnectors.components.pagerDuty.linkURLFieldLabel',
{
defaultMessage: 'URL',
}
)}
fullWidth
>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value, actionIndex) => {
const newLinks = [...links];
newLinks[currentLinkIndex] = { text: link.text, href: value };
editAction('links', newLinks, actionIndex);
}}
messageVariables={messageVariables}
paramsProperty={'linksHref'}
inputTargetValue={link.href}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.stackConnectors.components.pagerDuty.linkTextFieldLabel',
{
defaultMessage: 'Label',
}
)}
fullWidth
>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value, actionIndex) => {
const newLinks = [...links];
newLinks[currentLinkIndex] = { href: link.href, text: value };
editAction('links', newLinks, actionIndex);
}}
messageVariables={messageVariables}
paramsProperty={'linksText'}
inputTargetValue={link.text}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
onClick={() => {
links.splice(currentLinkIndex, 1);
editAction('links', links, index);
}}
iconType="minusInCircle"
css={{ marginTop: 28 }}
data-test-subj="pagerDutyRemoveLinkButton"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
<EuiFlexItem>
<div>
<EuiButton
iconType="plusInCircle"
onClick={() =>
editAction(
'links',
links ? [...links, { href: '', text: '' }] : [{ href: '', text: '' }],
index
)
}
data-test-subj="pagerDutyAddLinkButton"
>
{i18n.translate('xpack.stackConnectors.components.pagerDuty.addLinkButtonLabel', {
defaultMessage: 'Add Link',
})}
</EuiButton>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ describe('pagerduty action params validation', () => {
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{}',
links: [],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: [],
customDetails: [],
},
});
});
Expand All @@ -74,6 +78,142 @@ describe('pagerduty action params validation', () => {
dedupKey: [],
summary: [],
timestamp: expect.arrayContaining(expected),
links: [],
customDetails: [],
},
});
});

test('action params validation fails when customDetails are not valid JSON', async () => {
const actionParams = {
eventAction: 'trigger',
dedupKey: 'test',
summary: '2323',
source: 'source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{foo:bar}',
links: [],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: [],
customDetails: ['Custom details must be a valid JSON.'],
},
});
});

test('action params validation does not fail when customDetails are not JSON but have mustache templates inside', async () => {
const actionParams = {
eventAction: 'trigger',
dedupKey: 'test',
summary: '2323',
source: 'source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{"details": {{alert.flapping}}}',
links: [],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: [],
customDetails: [],
},
});
});

test('action params validation fails when a link is missing the href field', async () => {
const actionParams = {
eventAction: 'trigger',
dedupKey: 'test',
summary: '2323',
source: 'source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{}',
links: [{ href: '', text: 'foobar' }],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: ['Link properties cannot be empty.'],
customDetails: [],
},
});
});

test('action params validation fails when a link is missing the text field', async () => {
const actionParams = {
eventAction: 'trigger',
dedupKey: 'test',
summary: '2323',
source: 'source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{}',
links: [{ href: 'foobar', text: '' }],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: ['Link properties cannot be empty.'],
customDetails: [],
},
});
});

test('action params validation does not throw the same error multiple times for links', async () => {
const actionParams = {
eventAction: 'trigger',
dedupKey: 'test',
summary: '2323',
source: 'source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
customDetails: '{}',
links: [
{ href: 'foobar', text: '' },
{ href: '', text: 'foobar' },
{ href: '', text: '' },
],
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
dedupKey: [],
summary: [],
timestamp: [],
links: ['Link properties cannot be empty.'],
customDetails: [],
},
});
});
Expand Down
Loading

0 comments on commit 903149a

Please sign in to comment.