Merge pull request #8105 from keithjgrant/7877-notification-custom-messages

Notification Detail: show custom messages

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-09-15 18:53:41 +00:00
committed by GitHub
10 changed files with 293 additions and 77 deletions

View File

@@ -0,0 +1,35 @@
import 'styled-components/macro';
import React from 'react';
import styled from 'styled-components';
import { TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from './Detail';
const Value = styled(DetailValue)`
margin-top: var(--pf-global--spacer--xs);
padding: var(--pf-global--spacer--xs);
border: 1px solid var(--pf-global--BorderColor--100);
max-height: 5.5em;
overflow: auto;
`;
function ArrayDetail({ label, value, dataCy }) {
const labelCy = dataCy ? `${dataCy}-label` : null;
const valueCy = dataCy ? `${dataCy}-value` : null;
const vals = Array.isArray(value) ? value : [value];
return (
<div css="grid-column: span 2">
<DetailName component={TextListItemVariants.dt} data-cy={labelCy}>
{label}
</DetailName>
<Value component={TextListItemVariants.dd} data-cy={valueCy}>
{vals.map(v => (
<div>{v}</div>
))}
</Value>
</div>
);
}
export default ArrayDetail;

View File

@@ -1,11 +1,11 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React from 'react'; import React from 'react';
import { shape, node, number } from 'prop-types'; import { shape, node, number, oneOf } from 'prop-types';
import { TextListItemVariants } from '@patternfly/react-core'; import { TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from './Detail'; import { DetailName, DetailValue } from './Detail';
import CodeMirrorInput from '../CodeMirrorInput'; import CodeMirrorInput from '../CodeMirrorInput';
function ObjectDetail({ value, label, rows, fullHeight }) { function CodeDetail({ value, label, mode, rows, fullHeight }) {
return ( return (
<> <>
<DetailName <DetailName
@@ -28,8 +28,8 @@ function ObjectDetail({ value, label, rows, fullHeight }) {
css="grid-column: 1 / -1; margin-top: -20px" css="grid-column: 1 / -1; margin-top: -20px"
> >
<CodeMirrorInput <CodeMirrorInput
mode="json" mode={mode}
value={JSON.stringify(value)} value={value}
readOnly readOnly
rows={rows} rows={rows}
fullHeight={fullHeight} fullHeight={fullHeight}
@@ -39,13 +39,14 @@ function ObjectDetail({ value, label, rows, fullHeight }) {
</> </>
); );
} }
ObjectDetail.propTypes = { CodeDetail.propTypes = {
value: shape.isRequired, value: shape.isRequired,
label: node.isRequired, label: node.isRequired,
rows: number, rows: number,
mode: oneOf(['json', 'yaml', 'jinja2']).isRequired,
}; };
ObjectDetail.defaultProps = { CodeDetail.defaultProps = {
rows: null, rows: null,
}; };
export default ObjectDetail; export default CodeDetail;

View File

@@ -11,7 +11,7 @@ const DetailList = ({ children, stacked, ...props }) => (
export default styled(DetailList)` export default styled(DetailList)`
display: grid; display: grid;
grid-gap: 20px; grid-gap: 20px;
align-items: center; align-items: start;
${props => ${props =>
props.stacked props.stacked
? ` ? `

View File

@@ -3,8 +3,9 @@ export { default as Detail, DetailName, DetailValue } from './Detail';
export { default as DeletedDetail } from './DeletedDetail'; export { default as DeletedDetail } from './DeletedDetail';
export { default as UserDateDetail } from './UserDateDetail'; export { default as UserDateDetail } from './UserDateDetail';
export { default as DetailBadge } from './DetailBadge'; export { default as DetailBadge } from './DetailBadge';
export { default as ArrayDetail } from './ArrayDetail';
/* /*
NOTE: ObjectDetail cannot be imported here, as it causes circular NOTE: CodeDetail cannot be imported here, as it causes circular
dependencies in testing environment. Import it directly from dependencies in testing environment. Import it directly from
DetailList/ObjectDetail DetailList/ObjectDetail
*/ */

View File

@@ -407,12 +407,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "sc-bwzfXH", "componentId": "sc-bwzfXH",
"isStatic": false, "isStatic": false,
"lastClassName": "kVCDmm", "lastClassName": "gAzXep",
"rules": Array [ "rules": Array [
" "
display: grid; display: grid;
grid-gap: 20px; grid-gap: 20px;
align-items: center; align-items: start;
", ",
[Function], [Function],
" "
@@ -433,15 +433,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
stacked={true} stacked={true}
> >
<DetailList <DetailList
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
stacked={true} stacked={true}
> >
<TextList <TextList
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
component="dl" component="dl"
> >
<dl <dl
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
data-pf-content={true} data-pf-content={true}
> >
<Detail <Detail
@@ -630,12 +630,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "sc-bwzfXH", "componentId": "sc-bwzfXH",
"isStatic": false, "isStatic": false,
"lastClassName": "kVCDmm", "lastClassName": "gAzXep",
"rules": Array [ "rules": Array [
" "
display: grid; display: grid;
grid-gap: 20px; grid-gap: 20px;
align-items: center; align-items: start;
", ",
[Function], [Function],
" "
@@ -656,15 +656,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
stacked={true} stacked={true}
> >
<DetailList <DetailList
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
stacked={true} stacked={true}
> >
<TextList <TextList
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
component="dl" component="dl"
> >
<dl <dl
className="sc-bwzfXH kVCDmm" className="sc-bwzfXH gAzXep"
data-pf-content={true} data-pf-content={true}
> >
<Detail <Detail

View File

@@ -106,6 +106,7 @@ function NotificationTemplate({ setBreadcrumb, i18n }) {
<Route path="/notification_templates/:id/details"> <Route path="/notification_templates/:id/details">
<NotificationTemplateDetail <NotificationTemplateDetail
template={template} template={template}
defaultMessages={defaultMessages}
isLoading={isLoading} isLoading={isLoading}
/> />
</Route> </Route>

View File

@@ -7,22 +7,25 @@ import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card'; import { CardBody, CardActionsRow } from '../../../components/Card';
import { import {
Detail, Detail,
ArrayDetail,
DetailList, DetailList,
DeletedDetail, DeletedDetail,
} from '../../../components/DetailList'; } from '../../../components/DetailList';
import ObjectDetail from '../../../components/DetailList/ObjectDetail'; import CodeDetail from '../../../components/DetailList/CodeDetail';
import DeleteButton from '../../../components/DeleteButton'; import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import { NotificationTemplatesAPI } from '../../../api'; import { NotificationTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';
import hasCustomMessages from '../shared/hasCustomMessages';
import { NOTIFICATION_TYPES } from '../constants'; import { NOTIFICATION_TYPES } from '../constants';
function NotificationTemplateDetail({ i18n, template }) { function NotificationTemplateDetail({ i18n, template, defaultMessages }) {
const history = useHistory(); const history = useHistory();
const { const {
notification_configuration: configuration, notification_configuration: configuration,
summary_fields, summary_fields,
messages,
} = template; } = template;
const { request: deleteTemplate, isLoading, error: deleteError } = useRequest( const { request: deleteTemplate, isLoading, error: deleteError } = useRequest(
@@ -33,6 +36,7 @@ function NotificationTemplateDetail({ i18n, template }) {
); );
const { error, dismissError } = useDismissableError(deleteError); const { error, dismissError } = useDismissableError(deleteError);
const typeMessageDefaults = defaultMessages[template.notification_type];
return ( return (
<CardBody> <CardBody>
@@ -81,9 +85,9 @@ function NotificationTemplateDetail({ i18n, template }) {
value={configuration.host} value={configuration.host}
dataCy="nt-detail-host" dataCy="nt-detail-host"
/> />
<Detail <ArrayDetail
label={i18n._(t`Recipient List`)} label={i18n._(t`Recipient List`)}
value={configuration.recipients} // array value={configuration.recipients}
dataCy="nt-detail-recipients" dataCy="nt-detail-recipients"
/> />
<Detail <Detail
@@ -127,9 +131,9 @@ function NotificationTemplateDetail({ i18n, template }) {
value={configuration.panelId} value={configuration.panelId}
dataCy="nt-detail-panel-id" dataCy="nt-detail-panel-id"
/> />
<Detail <ArrayDetail
label={i18n._(t`Tags for the Annotation`)} label={i18n._(t`Tags for the Annotation`)}
value={configuration.annotation_tags} // array value={configuration.annotation_tags}
dataCy="nt-detail-" dataCy="nt-detail-"
/> />
<Detail <Detail
@@ -160,9 +164,9 @@ function NotificationTemplateDetail({ i18n, template }) {
value={configuration.nickname} value={configuration.nickname}
dataCy="nt-detail-irc-nickname" dataCy="nt-detail-irc-nickname"
/> />
<Detail <ArrayDetail
label={i18n._(t`Destination Channels or Users`)} label={i18n._(t`Destination Channels or Users`)}
value={configuration.targets} // array value={configuration.targets}
dataCy="nt-detail-channels" dataCy="nt-detail-channels"
/> />
<Detail <Detail
@@ -254,9 +258,9 @@ function NotificationTemplateDetail({ i18n, template }) {
)} )}
{template.notification_type === 'slack' && ( {template.notification_type === 'slack' && (
<> <>
<Detail <ArrayDetail
label={i18n._(t`Destination Channels`)} label={i18n._(t`Destination Channels`)}
value={configuration.channels} // array value={configuration.channels}
dataCy="nt-detail-slack-channels" dataCy="nt-detail-slack-channels"
/> />
<Detail <Detail
@@ -273,9 +277,9 @@ function NotificationTemplateDetail({ i18n, template }) {
value={configuration.from_number} value={configuration.from_number}
dataCy="nt-detail-twilio-source-phone" dataCy="nt-detail-twilio-source-phone"
/> />
<Detail <ArrayDetail
label={i18n._(t`Destination SMS Number`)} label={i18n._(t`Destination SMS Number(s)`)}
value={configuration.to_numbers} // array value={configuration.to_numbers}
dataCy="nt-detail-twilio-destination-numbers" dataCy="nt-detail-twilio-destination-numbers"
/> />
<Detail <Detail
@@ -311,14 +315,23 @@ function NotificationTemplateDetail({ i18n, template }) {
value={configuration.http_method} value={configuration.http_method}
dataCy="nt-detail-webhook-http-method" dataCy="nt-detail-webhook-http-method"
/> />
<ObjectDetail <CodeDetail
label={i18n._(t`HTTP Headers`)} label={i18n._(t`HTTP Headers`)}
value={configuration.headers} value={JSON.stringify(configuration.headers)}
mode="json"
rows="6" rows="6"
dataCy="nt-detail-webhook-headers" dataCy="nt-detail-webhook-headers"
/> />
</> </>
)} )}
{hasCustomMessages(messages, typeMessageDefaults) && (
<CustomMessageDetails
messages={messages}
defaults={typeMessageDefaults}
type={template.notification_type}
i18n={i18n}
/>
)}
</DetailList> </DetailList>
<CardActionsRow> <CardActionsRow>
{summary_fields.user_capabilities && {summary_fields.user_capabilities &&
@@ -358,4 +371,164 @@ function NotificationTemplateDetail({ i18n, template }) {
); );
} }
function CustomMessageDetails({ messages, defaults, type, i18n }) {
const showMessages = type !== 'webhook';
const showBodies = ['email', 'pagerduty', 'webhook'].includes(type);
return (
<>
{showMessages && (
<CodeDetail
label={i18n._(t`Start message`)}
value={messages.started.message || defaults.started.message}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Start message body`)}
value={messages.started.body || defaults.started.body}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Success message`)}
value={messages.success.message || defaults.success.message}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Success message body`)}
value={messages.success.body || defaults.success.body}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Error message`)}
value={messages.error.message || defaults.error.message}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Error message body`)}
value={messages.error.body || defaults.error.body}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Workflow approved message`)}
value={
messages.workflow_approval?.approved?.message ||
defaults.workflow_approval.approved.message
}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Workflow approved message body`)}
value={
messages.workflow_approval?.approved?.body ||
defaults.workflow_approval.approved.body
}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Workflow denied message`)}
value={
messages.workflow_approval?.denied?.message ||
defaults.workflow_approval.denied.message
}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Workflow denied message body`)}
value={
messages.workflow_approval?.denied?.body ||
defaults.workflow_approval.denied.body
}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Workflow pending message`)}
value={
messages.workflow_approval?.running?.message ||
defaults.workflow_approval.running.message
}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Workflow pending message body`)}
value={
messages.workflow_approval?.running?.body ||
defaults.workflow_approval.running.body
}
mode="jinja2"
rows="6"
fullWidth
/>
)}
{showMessages && (
<CodeDetail
label={i18n._(t`Workflow timed out message`)}
value={
messages.workflow_approval?.timed_out?.message ||
defaults.workflow_approval.timed_out.message
}
mode="jinja2"
rows="2"
fullWidth
/>
)}
{showBodies && (
<CodeDetail
label={i18n._(t`Workflow timed out message body`)}
value={
messages.workflow_approval?.timed_out?.body ||
defaults.workflow_approval.timed_out.body
}
mode="jinja2"
rows="6"
fullWidth
/>
)}
</>
);
}
export default withI18n()(NotificationTemplateDetail); export default withI18n()(NotificationTemplateDetail);

View File

@@ -124,7 +124,7 @@ function NotificationTemplatesList({ i18n }) {
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={() => setSelected([...templates])} onSelectAll={set => setSelected(set ? [...templates] : [])}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd

View File

@@ -13,6 +13,7 @@ import { required } from '../../../util/validators';
import { FormColumnLayout } from '../../../components/FormLayout'; import { FormColumnLayout } from '../../../components/FormLayout';
import TypeInputsSubForm from './TypeInputsSubForm'; import TypeInputsSubForm from './TypeInputsSubForm';
import CustomMessagesSubForm from './CustomMessagesSubForm'; import CustomMessagesSubForm from './CustomMessagesSubForm';
import hasCustomMessages from './hasCustomMessages';
import typeFieldNames, { initialConfigValues } from './typeFieldNames'; import typeFieldNames, { initialConfigValues } from './typeFieldNames';
function NotificationTemplateFormFields({ i18n, defaultMessages }) { function NotificationTemplateFormFields({ i18n, defaultMessages }) {
@@ -117,8 +118,8 @@ function NotificationTemplateForm({
const defs = defaultMessages[template.notification_type || 'email']; const defs = defaultMessages[template.notification_type || 'email'];
const mergeDefaultMessages = (templ = {}, def) => { const mergeDefaultMessages = (templ = {}, def) => {
return { return {
message: templ.message || def.message || '', message: templ?.message || def.message || '',
body: templ.body || def.body || '', body: templ?.body || def.body || '',
}; };
}; };
@@ -144,25 +145,25 @@ function NotificationTemplateForm({
workflow_approval: { workflow_approval: {
approved: { approved: {
...mergeDefaultMessages( ...mergeDefaultMessages(
messages.workflow_approval.approved, messages.workflow_approval?.approved,
defs.workflow_approval.approved defs.workflow_approval.approved
), ),
}, },
denied: { denied: {
...mergeDefaultMessages( ...mergeDefaultMessages(
messages.workflow_approval.denied, messages.workflow_approval?.denied,
defs.workflow_approval.denied defs.workflow_approval.denied
), ),
}, },
running: { running: {
...mergeDefaultMessages( ...mergeDefaultMessages(
messages.workflow_approval.running, messages.workflow_approval?.running,
defs.workflow_approval.running defs.workflow_approval.running
), ),
}, },
timed_out: { timed_out: {
...mergeDefaultMessages( ...mergeDefaultMessages(
messages.workflow_approval.timed_out, messages.workflow_approval?.timed_out,
defs.workflow_approval.timed_out defs.workflow_approval.timed_out
), ),
}, },
@@ -210,42 +211,6 @@ NotificationTemplateForm.defaultProps = {
export default withI18n()(NotificationTemplateForm); export default withI18n()(NotificationTemplateForm);
function hasCustomMessages(messages, defaults) {
return (
isCustomized(messages.started, defaults.started) ||
isCustomized(messages.success, defaults.success) ||
isCustomized(messages.error, defaults.error) ||
isCustomized(
messages.workflow_approval.approved,
defaults.workflow_approval.approved
) ||
isCustomized(
messages.workflow_approval.denied,
defaults.workflow_approval.denied
) ||
isCustomized(
messages.workflow_approval.running,
defaults.workflow_approval.running
) ||
isCustomized(
messages.workflow_approval.timed_out,
defaults.workflow_approval.timed_out
)
);
}
function isCustomized(message, defaultMessage) {
if (!message) {
return false;
}
if (message.message && message.message !== defaultMessage.message) {
return true;
}
if (message.body && message.body !== defaultMessage.body) {
return true;
}
return false;
}
function normalizeFields(values, defaultMessages) { function normalizeFields(values, defaultMessages) {
return normalizeTypeFields(normalizeMessageFields(values, defaultMessages)); return normalizeTypeFields(normalizeMessageFields(values, defaultMessages));
} }

View File

@@ -0,0 +1,40 @@
export default function hasCustomMessages(messages, defaults) {
if (!messages) {
return false;
}
return (
isCustomized(messages.started, defaults.started) ||
isCustomized(messages.success, defaults.success) ||
isCustomized(messages.error, defaults.error) ||
isCustomized(
messages.workflow_approval?.approved,
defaults.workflow_approval.approved
) ||
isCustomized(
messages.workflow_approval?.denied,
defaults.workflow_approval.denied
) ||
isCustomized(
messages.workflow_approval?.running,
defaults.workflow_approval.running
) ||
isCustomized(
messages.workflow_approval?.timed_out,
defaults.workflow_approval.timed_out
)
);
}
function isCustomized(message, defaultMessage) {
if (!message) {
return false;
}
if (message.message && message.message !== defaultMessage.message) {
return true;
}
if (message.body && message.body !== defaultMessage.body) {
return true;
}
return false;
}