Fixes bug where email notification ssl and tls were a dropdown instead of checkboxes

This commit is contained in:
mabashian
2021-06-16 16:50:02 -04:00
committed by Shane McDonald
parent 2ce09d0dcc
commit c993c8b3b9
9 changed files with 518 additions and 87 deletions

View File

@@ -15,7 +15,6 @@ import { FormSelect, FormSelectOption } from '@patternfly/react-core';
function AnsibleSelect({ function AnsibleSelect({
id, id,
data, data,
isValid, isValid,
onBlur, onBlur,
value, value,

View File

@@ -25,7 +25,7 @@ function ArrayDetail({ label, value, dataCy }) {
</DetailName> </DetailName>
<Value component={TextListItemVariants.dd} data-cy={valueCy}> <Value component={TextListItemVariants.dd} data-cy={valueCy}>
{vals.map(v => ( {vals.map(v => (
<div>{v}</div> <div key={v}>{v}</div>
))} ))}
</Value> </Value>
</div> </div>

View File

@@ -1,7 +1,12 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import {
import { Button } from '@patternfly/react-core'; Button,
TextList,
TextListItem,
TextListItemVariants,
TextListVariants,
} from '@patternfly/react-core';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card'; import { CardBody, CardActionsRow } from '../../../components/Card';
@@ -31,6 +36,23 @@ function NotificationTemplateDetail({ template, defaultMessages }) {
messages, messages,
} = template; } = template;
const renderOptionsField = configuration.use_ssl || configuration.use_tls;
const renderOptions = (
<TextList component={TextListVariants.ul}>
{configuration.use_ssl && (
<TextListItem component={TextListItemVariants.li}>
{t`Use SSL`}
</TextListItem>
)}
{configuration.use_tls && (
<TextListItem component={TextListItemVariants.li}>
{t`Use TLS`}
</TextListItem>
)}
</TextList>
);
const { request: deleteTemplate, isLoading, error: deleteError } = useRequest( const { request: deleteTemplate, isLoading, error: deleteError } = useRequest(
useCallback(async () => { useCallback(async () => {
await NotificationTemplatesAPI.destroy(template.id); await NotificationTemplatesAPI.destroy(template.id);
@@ -104,11 +126,9 @@ function NotificationTemplateDetail({ template, defaultMessages }) {
value={configuration.timeout} value={configuration.timeout}
dataCy="nt-detail-timeout" dataCy="nt-detail-timeout"
/> />
<Detail {renderOptionsField && (
label={t`Email Options`} <Detail label={t`Email Options`} value={renderOptions} />
value={configuration.use_ssl ? t`Use SSL` : t`Use TLS`} )}
dataCy="nt-detail-email-options"
/>
</> </>
)} )}
{template.notification_type === 'grafana' && ( {template.notification_type === 'grafana' && (

View File

@@ -0,0 +1,102 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import NotificationTemplateDetail from './NotificationTemplateDetail';
import defaultMessages from '../shared/notification-template-default-messages.json';
jest.mock('../../../api');
const mockTemplate = {
id: 1,
type: 'notification_template',
url: '/api/v2/notification_templates/1/',
related: {
named_url: '/api/v2/notification_templates/abc++Default/',
created_by: '/api/v2/users/2/',
modified_by: '/api/v2/users/2/',
test: '/api/v2/notification_templates/1/test/',
notifications: '/api/v2/notification_templates/1/notifications/',
copy: '/api/v2/notification_templates/1/copy/',
organization: '/api/v2/organizations/1/',
},
summary_fields: {
organization: {
id: 1,
name: 'Default',
description: '',
},
created_by: {
id: 2,
username: 'test',
first_name: '',
last_name: '',
},
modified_by: {
id: 2,
username: 'test',
first_name: '',
last_name: '',
},
user_capabilities: {
edit: true,
delete: true,
copy: true,
},
recent_notifications: [],
},
created: '2021-06-16T18:52:23.811374Z',
modified: '2021-06-16T18:53:37.631371Z',
name: 'abc',
description: 'foo description',
organization: 1,
notification_type: 'email',
notification_configuration: {
username: '',
password: '',
host: 'https://localhost',
recipients: ['foo@ansible.com'],
sender: 'bar@ansible.com',
port: 324,
timeout: 11,
use_ssl: true,
use_tls: true,
},
messages: null,
};
describe('<NotificationTemplateDetail />', () => {
let wrapper;
beforeEach(async () => {
await act(async () => {
wrapper = mountWithContexts(
<NotificationTemplateDetail
template={mockTemplate}
defaultMessages={defaultMessages}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
afterEach(() => {
jest.clearAllMocks();
});
test('should render Details', () => {
function assertDetail(label, value) {
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
}
assertDetail('Name', mockTemplate.name);
assertDetail('Description', mockTemplate.description);
expect(
wrapper
.find('Detail[label="Email Options"]')
.containsAllMatchingElements([<li>Use SSL</li>, <li>Use TLS</li>])
).toEqual(true);
});
});

View File

@@ -118,10 +118,6 @@ function NotificationTemplateForm({
); );
}; };
let emailOptions = '';
if (template.notification_type === 'email') {
emailOptions = template.notification_configuration?.use_ssl ? 'ssl' : 'tls';
}
const messages = template.messages || { workflow_approval: {} }; const messages = template.messages || { workflow_approval: {} };
const defs = defaultMessages[template.notification_type || 'email']; const defs = defaultMessages[template.notification_type || 'email'];
const mergeDefaultMessages = (templ = {}, def) => { const mergeDefaultMessages = (templ = {}, def) => {
@@ -144,7 +140,6 @@ function NotificationTemplateForm({
...template.notification_configuration, ...template.notification_configuration,
headers: headers ? JSON.stringify(headers, null, 2) : null, headers: headers ? JSON.stringify(headers, null, 2) : null,
}, },
emailOptions,
organization: template.summary_fields?.organization, organization: template.summary_fields?.organization,
messages: { messages: {
started: { ...mergeDefaultMessages(messages.started, defs.started) }, started: { ...mergeDefaultMessages(messages.started, defs.started) },
@@ -235,10 +230,6 @@ function normalizeTypeFields(values) {
stripped[fieldName] = values.notification_configuration[fieldName]; stripped[fieldName] = values.notification_configuration[fieldName];
} }
}); });
if (values.notification_type === 'email') {
stripped.use_ssl = values.emailOptions === 'ssl';
stripped.use_tls = !stripped.use_ssl;
}
if (values.notification_type === 'webhook') { if (values.notification_type === 'webhook') {
stripped.headers = stripped.headers ? JSON.parse(stripped.headers) : {}; stripped.headers = stripped.headers ? JSON.parse(stripped.headers) : {};
} }

View File

@@ -4,6 +4,7 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import NotificationTemplateForm from './NotificationTemplateForm'; import NotificationTemplateForm from './NotificationTemplateForm';
jest.mock('../../../api/models/NotificationTemplates'); jest.mock('../../../api/models/NotificationTemplates');
jest.mock('../../../api/models/Organizations');
const template = { const template = {
id: 3, id: 3,
@@ -50,16 +51,19 @@ const defaultMessages = {
}; };
describe('<NotificationTemplateForm />', () => { describe('<NotificationTemplateForm />', () => {
test('should render form fields', () => { let wrapper;
const wrapper = mountWithContexts( test('should render form fields', async () => {
<NotificationTemplateForm await act(async () => {
template={template} wrapper = mountWithContexts(
defaultMessages={defaultMessages} <NotificationTemplateForm
detailUrl="/notification_templates/3/detail" template={template}
onSubmit={jest.fn()} defaultMessages={defaultMessages}
onCancel={jest.fn()} detailUrl="/notification_templates/3/detail"
/> onSubmit={jest.fn()}
); onCancel={jest.fn()}
/>
);
});
expect(wrapper.find('input#notification-name').prop('value')).toEqual( expect(wrapper.find('input#notification-name').prop('value')).toEqual(
'Test Notification' 'Test Notification'
@@ -77,26 +81,48 @@ describe('<NotificationTemplateForm />', () => {
expect( expect(
wrapper.find('CustomMessagesSubForm').prop('defaultMessages') wrapper.find('CustomMessagesSubForm').prop('defaultMessages')
).toEqual(defaultMessages); ).toEqual(defaultMessages);
expect(wrapper.find('input#option-use-ssl').length).toBe(0);
expect(wrapper.find('input#option-use-tls').length).toBe(0);
await act(async () => {
wrapper.find('AnsibleSelect#notification-type').invoke('onChange')(
{
target: {
name: 'notification_type',
value: 'email',
},
},
'email'
);
});
wrapper.update();
expect(wrapper.find('input#option-use-ssl').length).toBe(1);
expect(wrapper.find('input#option-use-tls').length).toBe(1);
}); });
test('should render custom messages fields', () => { test('should render custom messages fields', async () => {
const wrapper = mountWithContexts( await act(async () => {
<NotificationTemplateForm wrapper = mountWithContexts(
template={{ <NotificationTemplateForm
...template, template={{
messages: { ...template,
started: { messages: {
message: 'Started', started: {
body: null, message: 'Started',
body: null,
},
}, },
}, }}
}} defaultMessages={defaultMessages}
defaultMessages={defaultMessages} detailUrl="/notification_templates/3/detail"
detailUrl="/notification_templates/3/detail" onSubmit={jest.fn()}
onSubmit={jest.fn()} onCancel={jest.fn()}
onCancel={jest.fn()} />
/> );
); });
expect( expect(
wrapper wrapper
@@ -108,21 +134,23 @@ describe('<NotificationTemplateForm />', () => {
test('should submit', async () => { test('should submit', async () => {
const handleSubmit = jest.fn(); const handleSubmit = jest.fn();
const wrapper = mountWithContexts( await act(async () => {
<NotificationTemplateForm wrapper = mountWithContexts(
template={{ <NotificationTemplateForm
...template, template={{
notification_configuration: { ...template,
channels: ['#foo'], notification_configuration: {
token: 'abc123', channels: ['#foo'],
}, token: 'abc123',
}} },
defaultMessages={defaultMessages} }}
detailUrl="/notification_templates/3/detail" defaultMessages={defaultMessages}
onSubmit={handleSubmit} detailUrl="/notification_templates/3/detail"
onCancel={jest.fn()} onSubmit={handleSubmit}
/> onCancel={jest.fn()}
); />
);
});
await act(async () => { await act(async () => {
wrapper.find('FormActionGroup').invoke('onSubmit')(); wrapper.find('FormActionGroup').invoke('onSubmit')();

View File

@@ -4,6 +4,7 @@ import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useField } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core'; import { FormGroup, Title } from '@patternfly/react-core';
import { import {
FormCheckboxLayout,
FormColumnLayout, FormColumnLayout,
FormFullWidthLayout, FormFullWidthLayout,
SubFormLayout, SubFormLayout,
@@ -56,10 +57,6 @@ TypeInputsSubForm.propTypes = {
export default TypeInputsSubForm; export default TypeInputsSubForm;
function EmailFields() { function EmailFields() {
const [optionsField, optionsMeta] = useField({
name: 'emailOptions',
validate: required(t`Select a value for this field`),
});
return ( return (
<> <>
<FormField <FormField
@@ -123,29 +120,19 @@ function EmailFields() {
notification stops trying to reach the host and times out. Ranges notification stops trying to reach the host and times out. Ranges
from 1 to 120 seconds.`} from 1 to 120 seconds.`}
/> />
<FormGroup <FormGroup fieldId="email-options" label={t`E-mail options`}>
fieldId="email-options" <FormCheckboxLayout>
helperTextInvalid={optionsMeta.error} <CheckboxField
isRequired id="option-use-ssl"
validated={ name="notification_configuration.use_ssl"
!optionsMeta.touched || !optionsMeta.error ? 'default' : 'error' label={t`Use SSL`}
} />
label={t`E-mail options`} <CheckboxField
> id="option-use-tls"
<AnsibleSelect name="notification_configuration.use_tls"
{...optionsField} label={t`Use TLS`}
id="email-options" />
data={[ </FormCheckboxLayout>
{
value: '',
key: '',
label: t`Choose an email option`,
isDisabled: true,
},
{ value: 'tls', key: 'tls', label: t`Use TLS` },
{ value: 'ssl', key: 'ssl', label: t`Use SSL` },
]}
/>
</FormGroup> </FormGroup>
</> </>
); );

View File

@@ -0,0 +1,302 @@
{
"type": "json",
"required": false,
"label": "Messages",
"help_text": "Optional custom messages for notification template.",
"filterable": true,
"default": {
"started": null,
"success": null,
"error": null,
"workflow_approval": null
},
"email": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}"
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}"
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}"
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}"
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}"
}
}
},
"slack": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": null
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": null
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": null
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": null
}
}
},
"twilio": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": null
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": null
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": null
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": null
}
}
},
"pagerduty": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_metadata }}"
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_metadata }}"
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": "{{ job_metadata }}"
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}"
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}"
}
}
},
"grafana": {
"started": {
"body": "{{ job_metadata }}",
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}"
},
"success": {
"body": "{{ job_metadata }}",
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}"
},
"error": {
"body": "{{ job_metadata }}",
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}"
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}"
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}"
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}"
}
}
},
"webhook": {
"started": {
"body": "{{ job_metadata }}"
},
"success": {
"body": "{{ job_metadata }}"
},
"error": {
"body": "{{ job_metadata }}"
},
"workflow_approval": {
"running": {
"body": {
"body": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}"
}
},
"approved": {
"body": {
"body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}"
}
},
"timed_out": {
"body": {
"body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}"
}
},
"denied": {
"body": {
"body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}"
}
}
}
},
"mattermost": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": null
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": null
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": null
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": null
}
}
},
"rocketchat": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": null
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": null
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": null
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": null
}
}
},
"irc": {
"started": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"success": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"error": {
"message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}",
"body": null
},
"workflow_approval": {
"running": {
"message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}",
"body": null
},
"approved": {
"message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}",
"body": null
},
"timed_out": {
"message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}",
"body": null
},
"denied": {
"message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}",
"body": null
}
}
}
}

View File

@@ -7,6 +7,8 @@ const typeFieldNames = {
'sender', 'sender',
'port', 'port',
'timeout', 'timeout',
'use_ssl',
'use_tls',
], ],
grafana: [ grafana: [
'grafana_url', 'grafana_url',