From 10a46063153e1a6938f2c6a8f394b4a7aad9fec7 Mon Sep 17 00:00:00 2001 From: Domas Date: Thu, 6 May 2021 12:32:45 +0300 Subject: [PATCH] Alerting: edit cloud receivers (#33570) --- .../alerting/components/OptionElement.tsx | 2 +- .../alerting/unified/Receivers.test.tsx | 305 +++++ .../alerting/unified/api/alertmanager.ts | 2 +- .../unified/components/AlertManagerPicker.tsx | 7 +- .../components/receivers/EditReceiverView.tsx | 3 +- .../components/receivers/NewReceiverView.tsx | 3 +- .../components/receivers/ReceiversSection.tsx | 9 +- .../receivers/ReceiversTable.test.tsx | 6 +- .../components/receivers/ReceiversTable.tsx | 5 +- .../components/receivers/TemplatesTable.tsx | 4 +- .../receivers/form/ChannelOptions.tsx | 68 +- .../receivers/form/ChannelSubForm.tsx | 39 +- .../form/CloudCommonChannelSettings.tsx | 19 + .../receivers/form/CloudReceiverForm.tsx | 78 ++ .../receivers/form/CollapsibleSection.tsx | 15 +- .../receivers/form/GrafanaReceiverForm.tsx | 3 +- .../receivers/form/OptionElement.tsx | 75 -- .../receivers/form/ReceiverForm.tsx | 56 +- .../receivers/form/SubformOptionElement.tsx | 0 .../receivers/form/fields/DeletedSubform.tsx | 21 + .../form/fields/KeyValueMapInput.tsx | 102 ++ .../receivers/form/fields/OptionField.tsx | 152 +++ .../form/fields/StringArrayInput.tsx | 72 ++ .../form/fields/SubformArrayField.tsx | 67 + .../receivers/form/fields/SubformField.tsx | 66 + .../receivers/form/fields/styles.ts | 30 + .../unified/components/rules/ActionIcon.tsx | 28 +- .../unified/components/rules/RulesGroup.tsx | 6 +- .../unified/components/rules/RulesTable.tsx | 4 +- .../components/silences/SilenceTableRow.tsx | 9 +- .../silences/SilencedAlertsTableRow.tsx | 2 +- .../unified/hooks/useControlledFieldArray.ts | 61 + public/app/features/alerting/unified/mocks.ts | 80 ++ .../unified/mocks/grafana-notifiers.ts | 1120 +++++++++++++++++ .../alerting/unified/state/actions.ts | 9 +- .../alerting/unified/types/receiver-form.ts | 21 +- .../unified/utils/alertmanager-config.ts | 18 + .../cloud-alertmanager-notifier-types.ts | 332 +++++ .../unified/utils/receiver-form.test.ts | 44 + .../alerting/unified/utils/receiver-form.ts | 110 +- .../features/alerting/unified/utils/redux.ts | 23 +- public/app/types/alerting.ts | 26 +- 42 files changed, 2906 insertions(+), 196 deletions(-) create mode 100644 public/app/features/alerting/unified/Receivers.test.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/CloudCommonChannelSettings.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx delete mode 100644 public/app/features/alerting/unified/components/receivers/form/OptionElement.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/SubformOptionElement.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/StringArrayInput.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx create mode 100644 public/app/features/alerting/unified/components/receivers/form/fields/styles.ts create mode 100644 public/app/features/alerting/unified/hooks/useControlledFieldArray.ts create mode 100644 public/app/features/alerting/unified/mocks/grafana-notifiers.ts create mode 100644 public/app/features/alerting/unified/utils/alertmanager-config.ts create mode 100644 public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts create mode 100644 public/app/features/alerting/unified/utils/receiver-form.test.ts diff --git a/public/app/features/alerting/components/OptionElement.tsx b/public/app/features/alerting/components/OptionElement.tsx index d02b851bcb2..47306dc5878 100644 --- a/public/app/features/alerting/components/OptionElement.tsx +++ b/public/app/features/alerting/components/OptionElement.tsx @@ -29,7 +29,7 @@ export const OptionElement: FC = ({ control, option, register, invalid }) control={control} name={`${modelValue}`} render={({ field: { ref, ...field } }) => ( - )} /> ); diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx new file mode 100644 index 00000000000..76b8788c926 --- /dev/null +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -0,0 +1,305 @@ +import { configureStore } from 'app/store/configureStore'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import Receivers from './Receivers'; +import React from 'react'; +import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { act, render } from '@testing-library/react'; +import { getAllDataSources } from './utils/config'; +import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; +import { updateAlertManagerConfig, fetchAlertManagerConfig } from './api/alertmanager'; +import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someGrafanaAlertManagerConfig } from './mocks'; +import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { fetchNotifiers } from './api/grafana'; +import { grafanaNotifiersMock } from './mocks/grafana-notifiers'; +import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; +import userEvent from '@testing-library/user-event'; +import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; +import store from 'app/core/store'; + +jest.mock('./api/alertmanager'); +jest.mock('./api/grafana'); +jest.mock('./utils/config'); + +const mocks = { + getAllDataSources: typeAsJestMock(getAllDataSources), + + api: { + fetchConfig: typeAsJestMock(fetchAlertManagerConfig), + updateConfig: typeAsJestMock(updateAlertManagerConfig), + fetchNotifiers: typeAsJestMock(fetchNotifiers), + }, +}; + +const renderReceivers = (alertManagerSourceName?: string) => { + const store = configureStore(); + + locationService.push( + '/alerting/notifications' + + (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '') + ); + + return render( + + + + + + ); +}; + +const dataSources = { + alertManager: mockDataSource({ + name: 'CloudManager', + type: DataSourceType.Alertmanager, + }), +}; + +const ui = { + newContactPointButton: byRole('link', { name: /new contact point/i }), + saveContactButton: byRole('button', { name: /save contact point/i }), + newContactPointTypeButton: byRole('button', { name: /new contact point type/i }), + + receiversTable: byTestId('receivers-table'), + templatesTable: byTestId('templates-table'), + alertManagerPicker: byTestId('alertmanager-picker'), + + channelFormContainer: byTestId('item-container'), + + inputs: { + name: byLabelText('Name'), + email: { + addresses: byLabelText('Addresses'), + }, + hipchat: { + url: byLabelText('Hip Chat Url'), + apiKey: byLabelText('API Key'), + }, + slack: { + webhookURL: byLabelText(/Webhook URL/i), + }, + webhook: { + URL: byLabelText(/The endpoint to send HTTP POST requests to/i), + }, + }, +}; + +const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => { + userEvent.click(byRole('textbox').get(selectElement)); + userEvent.click(byText(optionText).get(selectElement)); +}; + +describe('Receivers', () => { + beforeEach(() => { + jest.resetAllMocks(); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); + }); + + it('Template and receiver tables are rendered, alert manager can be selected', async () => { + mocks.api.fetchConfig.mockImplementation((name) => + Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig) + ); + await renderReceivers(); + + // check that by default grafana templates & receivers are fetched rendered in appropriate tables + let receiversTable = await ui.receiversTable.find(); + let templatesTable = await ui.templatesTable.find(); + let templateRows = templatesTable.querySelectorAll('tbody tr'); + expect(templateRows).toHaveLength(3); + expect(templateRows[0]).toHaveTextContent('first template'); + expect(templateRows[1]).toHaveTextContent('second template'); + expect(templateRows[2]).toHaveTextContent('third template'); + let receiverRows = receiversTable.querySelectorAll('tbody tr'); + expect(receiverRows[0]).toHaveTextContent('default'); + expect(receiverRows[1]).toHaveTextContent('critical'); + expect(receiverRows).toHaveLength(2); + + expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1); + expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME); + expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1); + expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined); + + // select external cloud alertmanager, check that data is retrieved and contents are rendered as appropriate + await clickSelectOption(ui.alertManagerPicker.get(), 'CloudManager'); + await byText('cloud-receiver').find(); + expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2); + expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager'); + + receiversTable = await ui.receiversTable.find(); + templatesTable = await ui.templatesTable.find(); + templateRows = templatesTable.querySelectorAll('tbody tr'); + expect(templateRows[0]).toHaveTextContent('foo template'); + expect(templateRows).toHaveLength(1); + receiverRows = receiversTable.querySelectorAll('tbody tr'); + expect(receiverRows[0]).toHaveTextContent('cloud-receiver'); + expect(receiverRows).toHaveLength(1); + expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual('CloudManager'); + }); + + it('Grafana receiver can be created', async () => { + mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); + mocks.api.updateConfig.mockResolvedValue(); + await renderReceivers(); + + // go to new contact point page + await userEvent.click(await ui.newContactPointButton.find()); + + await byRole('heading', { name: /create contact point/i }).find(); + expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new'); + + // type in a name for the new receiver + await userEvent.type(byLabelText('Name').get(), 'my new receiver'); + + // check that default email form is rendered + await ui.inputs.name.find(); + + // select hipchat + clickSelectOption(byTestId('items.0.type').get(), 'HipChat'); + + // check that email options are gone and hipchat options appear + expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument(); + + const urlInput = ui.inputs.hipchat.url.get(); + const apiKeyInput = ui.inputs.hipchat.apiKey.get(); + + await userEvent.type(urlInput, 'http://hipchat'); + await userEvent.type(apiKeyInput, 'foobarbaz'); + + // it seems react-hook-form does some async state updates after submit + await act(async () => { + await userEvent.click(ui.saveContactButton.get()); + }); + + // see that we're back to main page and proper api calls have been made + await ui.receiversTable.find(); + expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1); + expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3); + expect(locationService.getLocation().pathname).toEqual('/alerting/notifications'); + expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, { + ...someGrafanaAlertManagerConfig, + alertmanager_config: { + ...someGrafanaAlertManagerConfig.alertmanager_config, + receivers: [ + ...(someGrafanaAlertManagerConfig.alertmanager_config.receivers ?? []), + { + name: 'my new receiver', + grafana_managed_receiver_configs: [ + { + disableResolveMessage: false, + name: 'my new receiver', + secureSettings: {}, + sendReminder: true, + settings: { + apiKey: 'foobarbaz', + roomid: '', + url: 'http://hipchat', + }, + type: 'hipchat', + }, + ], + }, + ], + }, + }); + }); + + it('Cloud alertmanager receiver can be edited', async () => { + mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); + mocks.api.updateConfig.mockResolvedValue(); + await renderReceivers('CloudManager'); + + // click edit button for the receiver + const receiversTable = await ui.receiversTable.find(); + const receiverRows = receiversTable.querySelectorAll('tbody tr'); + expect(receiverRows[0]).toHaveTextContent('cloud-receiver'); + await userEvent.click(byTestId('edit').get(receiverRows[0])); + + // check that form is open + await byRole('heading', { name: /update contact point/i }).find(); + expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit'); + expect(ui.channelFormContainer.queryAll()).toHaveLength(2); + + // delete the email channel + expect(ui.channelFormContainer.queryAll()).toHaveLength(2); + await userEvent.click(byTestId('items.0.delete-button').get()); + expect(ui.channelFormContainer.queryAll()).toHaveLength(1); + + // modify webhook url + const slackContainer = ui.channelFormContainer.get(); + await userEvent.click(byText('Optional Slack settings').get(slackContainer)); + userEvent.type(ui.inputs.slack.webhookURL.get(slackContainer), 'http://newgreaturl'); + + // add confirm button to action + await userEvent.click(byText(/Actions \(1\)/i).get(slackContainer)); + await userEvent.click(await byTestId('items.1.settings.actions.0.confirm.add-button').find()); + const confirmSubform = byTestId('items.1.settings.actions.0.confirm.container').get(); + await userEvent.type(byLabelText('Text').get(confirmSubform), 'confirm this'); + + // delete a field + await userEvent.click(byText(/Fields \(2\)/i).get(slackContainer)); + await userEvent.click(byTestId('items.1.settings.fields.0.delete-button').get()); + await byText(/Fields \(1\)/i).get(slackContainer); + + // add another channel + await userEvent.click(ui.newContactPointTypeButton.get()); + await clickSelectOption(await byTestId('items.2.type').find(), 'Webhook'); + await userEvent.type(await ui.inputs.webhook.URL.find(), 'http://webhookurl'); + + // it seems react-hook-form does some async state updates after submit + await act(async () => { + await userEvent.click(ui.saveContactButton.get()); + }); + + // see that we're back to main page and proper api calls have been made + await ui.receiversTable.find(); + expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1); + expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3); + expect(locationService.getLocation().pathname).toEqual('/alerting/notifications'); + expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', { + ...someCloudAlertManagerConfig, + alertmanager_config: { + ...someCloudAlertManagerConfig.alertmanager_config, + receivers: [ + { + name: 'cloud-receiver', + slack_configs: [ + { + actions: [ + { + confirm: { + text: 'confirm this', + }, + text: 'action1text', + type: 'action1type', + url: 'http://action1', + }, + ], + api_url: 'http://slack1http://newgreaturl', + channel: '#mychannel', + fields: [ + { + short: false, + title: 'field2', + value: 'text2', + }, + ], + link_names: false, + send_resolved: false, + short_fields: false, + }, + ], + webhook_configs: [ + { + send_resolved: true, + url: 'http://webhookurl', + }, + ], + }, + ], + }, + }); + }, 10000); +}); diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 944cb2f6dbd..35be36a3e7e 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -40,7 +40,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P } } -export async function updateAlertmanagerConfig( +export async function updateAlertManagerConfig( alertManagerSourceName: string, config: AlertManagerCortexConfig ): Promise { diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index 59984cd1381..4ba5083af21 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -39,7 +39,12 @@ export const AlertManagerPicker: FC = ({ onChange, current, disabled = fa } return ( - + ({ } /> - ) : ( - - )} - + + ); + } + + const error: FieldError | DeepMap | undefined = ((option.secure + ? errors?.secureSettings + : errors?.settings) as DeepMap | undefined)?.[option.propertyName]; + + const defaultValue = defaultValues?.settings?.[option.propertyName]; + + return ( + ); })} diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 07b05eabb96..f3db81fd215 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -1,6 +1,6 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { NotifierDTO } from 'app/types'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui'; import { useFormContext, FieldErrors } from 'react-hook-form'; @@ -32,9 +32,13 @@ export function ChannelSubForm({ }: Props): JSX.Element { const styles = useStyles2(getStyles); const name = (fieldName: string) => `${pathPrefix}${fieldName}`; - const { control, watch } = useFormContext(); + const { control, watch, register } = useFormContext(); const selectedType = watch(name('type')) ?? defaultValues.type; // nope, setting "default" does not work at all. + useEffect(() => { + register(`${pathPrefix}.__id`); + }, [register, pathPrefix]); + const [_secureFields, setSecureFields] = useState(secureFields ?? {}); const onResetSecureField = (key: string) => { @@ -47,10 +51,12 @@ export function ChannelSubForm({ const typeOptions = useMemo( (): SelectableValue[] => - notifiers.map(({ name, type }) => ({ - label: name, - value: type, - })), + notifiers + .map(({ name, type }) => ({ + label: name, + value: type, + })) + .sort((a, b) => a.label.localeCompare(b.label)), [notifiers] ); @@ -61,16 +67,10 @@ export function ChannelSubForm({ const optionalOptions = notifier?.options.filter((o) => !o.required); return ( -
+
- } - defaultValue={defaultValues.__id} - control={control} - /> - + ({ Duplicate {onDelete && ( - )} @@ -96,6 +103,7 @@ export function ChannelSubForm({ {notifier && (
+ defaultValues={defaultValues} selectedChannelOptions={mandatoryOptions?.length ? mandatoryOptions! : optionalOptions!} secureFields={_secureFields} errors={errors} @@ -110,6 +118,7 @@ export function ChannelSubForm({ )} + defaultValues={defaultValues} selectedChannelOptions={optionalOptions!} secureFields={_secureFields} onResetSecureField={onResetSecureField} diff --git a/public/app/features/alerting/unified/components/receivers/form/CloudCommonChannelSettings.tsx b/public/app/features/alerting/unified/components/receivers/form/CloudCommonChannelSettings.tsx new file mode 100644 index 00000000000..1430965dc38 --- /dev/null +++ b/public/app/features/alerting/unified/components/receivers/form/CloudCommonChannelSettings.tsx @@ -0,0 +1,19 @@ +import { Checkbox, Field } from '@grafana/ui'; +import React, { FC } from 'react'; +import { CommonSettingsComponentProps } from '../../../types/receiver-form'; +import { useFormContext } from 'react-hook-form'; + +export const CloudCommonChannelSettings: FC = ({ pathPrefix, className }) => { + const { register } = useFormContext(); + return ( +
+ + + +
+ ); +}; diff --git a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx new file mode 100644 index 00000000000..7a673f72383 --- /dev/null +++ b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx @@ -0,0 +1,78 @@ +import { Alert } from '@grafana/ui'; +import { AlertManagerCortexConfig, Receiver } from 'app/plugins/datasource/alertmanager/types'; +import React, { FC, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateAlertManagerConfigAction } from '../../../state/actions'; +import { CloudChannelValues, ReceiverFormValues, CloudChannelMap } from '../../../types/receiver-form'; +import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types'; +import { makeAMLink } from '../../../utils/misc'; +import { + cloudReceiverToFormValues, + formValuesToCloudReceiver, + updateConfigWithReceiver, +} from '../../../utils/receiver-form'; +import { CloudCommonChannelSettings } from './CloudCommonChannelSettings'; +import { ReceiverForm } from './ReceiverForm'; + +interface Props { + alertManagerSourceName: string; + config: AlertManagerCortexConfig; + existing?: Receiver; +} + +const defaultChannelValues: CloudChannelValues = Object.freeze({ + __id: '', + sendResolved: true, + secureSettings: {}, + settings: {}, + secureFields: {}, + type: 'email', +}); + +export const CloudReceiverForm: FC = ({ existing, alertManagerSourceName, config }) => { + const dispatch = useDispatch(); + + // transform receiver DTO to form values + const [existingValue] = useMemo((): [ReceiverFormValues | undefined, CloudChannelMap] => { + if (!existing) { + return [undefined, {}]; + } + return cloudReceiverToFormValues(existing, cloudNotifierTypes); + }, [existing]); + + const onSubmit = (values: ReceiverFormValues) => { + const newReceiver = formValuesToCloudReceiver(values, defaultChannelValues); + dispatch( + updateAlertManagerConfigAction({ + newConfig: updateConfigWithReceiver(config, newReceiver, existing?.name), + oldConfig: config, + alertManagerSourceName, + successMessage: existing ? 'Contact point updated.' : 'Contact point created.', + redirectPath: makeAMLink('/alerting/notifications', alertManagerSourceName), + }) + ); + }; + + const takenReceiverNames = useMemo( + () => config.alertmanager_config.receivers?.map(({ name }) => name).filter((name) => name !== existing?.name) ?? [], + [config, existing] + ); + + return ( + <> + + Note that empty string values will be replaced with global defaults were appropriate. + + + config={config} + onSubmit={onSubmit} + initialValues={existingValue} + notifiers={cloudNotifierTypes} + alertManagerSourceName={alertManagerSourceName} + defaultItem={defaultChannelValues} + takenReceiverNames={takenReceiverNames} + commonSettingsComponent={CloudCommonChannelSettings} + /> + + ); +}; diff --git a/public/app/features/alerting/unified/components/receivers/form/CollapsibleSection.tsx b/public/app/features/alerting/unified/components/receivers/form/CollapsibleSection.tsx index d4d6e3d9fe0..16a1470f244 100644 --- a/public/app/features/alerting/unified/components/receivers/form/CollapsibleSection.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/CollapsibleSection.tsx @@ -1,24 +1,27 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; import React, { FC, useState } from 'react'; interface Props { label: string; + description?: string; + className?: string; } -export const CollapsibleSection: FC = ({ label, children }) => { +export const CollapsibleSection: FC = ({ label, description, children, className }) => { const styles = useStyles2(getStyles); const [isCollapsed, setIsCollapsed] = useState(true); const toggleCollapse = () => setIsCollapsed(!isCollapsed); return ( -
+
{label}
+ {description &&

{description}

}
{children}
); @@ -41,4 +44,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ hidden: css` display: none; `, + description: css` + color: ${theme.colors.text.secondary}; + font-size: ${theme.typography.size.sm}; + font-weight: ${theme.typography.fontWeightRegular}; + margin: 0; + `, }); diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 6f5577d4b11..6029e2c1c32 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -63,7 +63,7 @@ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceNam newConfig: updateConfigWithReceiver(config, newReceiver, existing?.name), oldConfig: config, alertManagerSourceName: GRAFANA_RULES_SOURCE_NAME, - successMessage: existing ? 'Receiver updated.' : 'Receiver created', + successMessage: existing ? 'Contact point updated.' : 'Contact point created', redirectPath: '/alerting/notifications', }) ); @@ -77,6 +77,7 @@ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceNam if (grafanaNotifiers.result) { return ( + config={config} onSubmit={onSubmit} initialValues={existingValue} notifiers={grafanaNotifiers.result} diff --git a/public/app/features/alerting/unified/components/receivers/form/OptionElement.tsx b/public/app/features/alerting/unified/components/receivers/form/OptionElement.tsx deleted file mode 100644 index 6db9a658f4f..00000000000 --- a/public/app/features/alerting/unified/components/receivers/form/OptionElement.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FC, useEffect } from 'react'; -import { Input, InputControl, Select, TextArea } from '@grafana/ui'; -import { NotificationChannelOption } from 'app/types'; -import { useFormContext } from 'react-hook-form'; - -interface Props { - option: NotificationChannelOption; - invalid?: boolean; - pathPrefix?: string; -} - -export const OptionElement: FC = ({ option, invalid, pathPrefix = '' }) => { - const { control, register, unregister } = useFormContext(); - const modelValue = option.secure - ? `${pathPrefix}secureSettings.${option.propertyName}` - : `${pathPrefix}settings.${option.propertyName}`; - - // workaround for https://github.com/react-hook-form/react-hook-form/issues/4993#issuecomment-829012506 - useEffect( - () => () => { - unregister(modelValue); - }, - [unregister, modelValue] - ); - - switch (option.element) { - case 'input': - return ( - (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), - })} - placeholder={option.placeholder} - /> - ); - - case 'select': - return ( - ( -