diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx index 6844c98cf3f..148e14e3ec7 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx @@ -5,17 +5,27 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -export interface RadioButtonDotProps { +export interface RadioButtonDotProps { id: string; name: string; checked?: boolean; + value?: T; disabled?: boolean; label: React.ReactNode; description?: string; onChange?: (id: string) => void; } -export const RadioButtonDot = ({ id, name, label, checked, disabled, description, onChange }: RadioButtonDotProps) => { +export const RadioButtonDot = ({ + id, + name, + label, + checked, + value, + disabled, + description, + onChange, +}: RadioButtonDotProps) => { const styles = useStyles2(getStyles); return ( @@ -25,11 +35,15 @@ export const RadioButtonDot = ({ id, name, label, checked, disabled, description name={name} type="radio" checked={checked} + value={value} disabled={disabled} className={styles.input} onChange={() => onChange && onChange(id)} /> - {label} +
+ {label} + {description &&
{description}
} +
); }; @@ -84,4 +98,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ gridTemplateColumns: `${theme.spacing(2)} auto`, gap: theme.spacing(1), }), + description: css({ + fontSize: theme.typography.size.sm, + color: theme.colors.text.secondary, + }), }); diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.mdx b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.mdx index b90ce9b1675..d2a17c19207 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.mdx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.mdx @@ -43,6 +43,30 @@ const options = [ const disabledOptions = ['prometheus', 'elastic']; + +``` + +### Options with descriptions + +You can add descriptions to the options by passing them to the option's description property. +Descriptions should be short and concise. Try to avoid multiline text. + +```jsx +import { RadioButtonList } from '@grafana/ui'; + +const options = [ + { label: 'Prometheus', value: 'prometheus', description: 'Monitoring system & TSDB' }, + { label: 'Loki', value: 'loki', description: 'Log aggregation system' }, +]; + +const disabledOptions = ['prometheus', 'elastic']; + + > = [ - { label: 'Option 1', value: 'opt-1', description: 'A description of Option 1' }, - { label: 'Option 2', value: 'opt-2', description: 'A description of Option 2' }, - { label: 'Option 3', value: 'opt-3', description: 'A description of Option 3' }, - { label: 'Option 4', value: 'opt-4', description: 'A description of Option 4' }, - { label: 'Option 5', value: 'opt-5', description: 'A description of Option 5' }, + { label: 'Option 1', value: 'opt-1' }, + { label: 'Option 2', value: 'opt-2' }, + { label: 'Option 3', value: 'opt-3' }, + { label: 'Option 4', value: 'opt-4' }, + { label: 'Option 5', value: 'opt-5' }, ]; const meta: Meta = { @@ -81,6 +81,18 @@ export const LongLabels: StoryFn = ({ disabled, disabled ); +export const WithDescriptions: StoryFn = ({ disabled, disabledOptions }) => ( +
+ +
+); + export const ControlledComponent: Story> = ({ disabled, disabledOptions }) => { const [selected, setSelected] = useState(defaultOptions[0].value!); diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx index f627e29a66f..a0c0d32bf41 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx @@ -23,7 +23,7 @@ export interface RadioButtonListProps { className?: string; } -export function RadioButtonList({ +export function RadioButtonList({ name, id, options, @@ -47,13 +47,14 @@ export function RadioButtonList({ const handleChange = () => onChange && option.value && onChange(option.value); return ( - key={index} id={itemId} name={name} label={option.label} description={option.description} checked={isChecked} + value={option.value} disabled={isDisabled} onChange={handleChange} /> diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index a2564f5fb27..99649b2db29 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -1,6 +1,6 @@ import { dateTime } from '@grafana/data'; import { faro, LogLevel as GrafanaLogLevel } from '@grafana/faro-web-sdk'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, logError } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime/src'; import { contextSrv } from 'app/core/core'; @@ -30,6 +30,10 @@ export function logInfo(message: string, context: Record = {}) { + logError(error, { ...context, module: 'Alerting' }); +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withPerformanceLogging Promise>( func: TFunc, diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts index fd7625e3c09..b27f92d1e7c 100644 --- a/public/app/features/alerting/unified/api/alertingApi.ts +++ b/public/app/features/alerting/unified/api/alertingApi.ts @@ -27,6 +27,6 @@ export const backendSrvBaseQuery = (): BaseQueryFn => async ( export const alertingApi = createApi({ reducerPath: 'alertingApi', baseQuery: backendSrvBaseQuery(), - tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration'], + tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration', 'OnCallIntegrations'], endpoints: () => ({}), }); diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index 238f28d0ff3..e97588813de 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -13,6 +13,7 @@ import { ExternalAlertmanagersResponse, Matcher, } from '../../../../plugins/datasource/alertmanager/types'; +import { NotifierDTO } from '../../../../types'; import { withPerformanceLogging } from '../Analytics'; import { matcherToOperator } from '../utils/alertmanager'; import { @@ -83,6 +84,10 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ }), }), + grafanaNotifiers: build.query({ + query: () => ({ url: '/api/alert-notifiers' }), + }), + getAlertmanagerChoiceStatus: build.query({ query: () => ({ url: '/api/v1/ngalert' }), providesTags: ['AlertmanagerChoice'], diff --git a/public/app/features/alerting/unified/api/onCallApi.ts b/public/app/features/alerting/unified/api/onCallApi.ts index a7272211950..7127f4cfbe0 100644 --- a/public/app/features/alerting/unified/api/onCallApi.ts +++ b/public/app/features/alerting/unified/api/onCallApi.ts @@ -1,36 +1,89 @@ -import { lastValueFrom } from 'rxjs'; +import { FetchError, isFetchError } from '@grafana/runtime'; -import { getBackendSrv } from '@grafana/runtime'; +import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall'; +import { SupportedPlugin } from '../types/pluginBridges'; import { alertingApi } from './alertingApi'; -export interface OnCallIntegration { + +export interface NewOnCallIntegrationDTO { + id: string; + connected_escalations_chains_count: number; + integration: string; + integration_url: string; + verbal_name: string; +} + +export interface OnCallPaginatedResult { + results: T[]; +} + +export const ONCALL_INTEGRATION_V2_FEATURE = 'grafana_alerting_v2'; +type OnCallFeature = typeof ONCALL_INTEGRATION_V2_FEATURE | string; + +type AlertReceiveChannelsResult = OnCallPaginatedResult | OnCallIntegrationDTO[]; + +export interface OnCallIntegrationDTO { + value: string; + display_name: string; integration_url: string; } -export type OnCallIntegrationsResponse = OnCallIntegration[]; -export type OnCallIntegrationsUrls = string[]; + +export interface CreateIntegrationDTO { + integration: typeof GRAFANA_ONCALL_INTEGRATION_TYPE; // The only one supported right now + verbal_name: string; +} + +const getProxyApiUrl = (path: string) => `/api/plugin-proxy/${SupportedPlugin.OnCall}${path}`; export const onCallApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ - getOnCallIntegrations: build.query({ - queryFn: async () => { - const integrations = await fetchOnCallIntegrations(); - return { data: integrations }; + grafanaOnCallIntegrations: build.query({ + query: () => ({ + url: getProxyApiUrl('/api/internal/v1/alert_receive_channels/'), + // legacy_grafana_alerting is necessary for OnCall. + // We do NOT need to differentiate between these two on our side + params: { filters: true, integration: [GRAFANA_ONCALL_INTEGRATION_TYPE, 'legacy_grafana_alerting'] }, + }), + transformResponse: (response: AlertReceiveChannelsResult) => { + if (isPaginatedResponse(response)) { + return response.results; + } + return response; }, + providesTags: ['OnCallIntegrations'], + }), + validateIntegrationName: build.query({ + query: (name) => ({ + url: getProxyApiUrl('/api/internal/v1/alert_receive_channels/validate_name/'), + params: { verbal_name: name }, + showErrorAlert: false, + }), + }), + createIntegration: build.mutation({ + query: (integration) => ({ + url: getProxyApiUrl('/api/internal/v1/alert_receive_channels/'), + data: integration, + method: 'POST', + showErrorAlert: true, + }), + invalidatesTags: ['OnCallIntegrations'], + }), + features: build.query({ + query: () => ({ + url: getProxyApiUrl('/api/internal/v1/features/'), + }), }), }), }); -export async function fetchOnCallIntegrations(): Promise { - try { - const response = await lastValueFrom( - getBackendSrv().fetch({ - url: '/api/plugin-proxy/grafana-oncall-app/api/internal/v1/alert_receive_channels/', - showErrorAlert: false, - showSuccessAlert: false, - }) - ); - return response.data.map((result) => result.integration_url); - } catch (error) { - return []; - } + +function isPaginatedResponse( + response: AlertReceiveChannelsResult +): response is OnCallPaginatedResult { + return 'results' in response && Array.isArray(response.results); +} + +export const { useGrafanaOnCallIntegrationsQuery } = onCallApi; + +export function isOnCallFetchError(error: unknown): error is FetchError<{ detail: string }> { + return isFetchError(error) && 'detail' in error.data; } -export const { useGetOnCallIntegrationsQuery } = onCallApi; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx index 5fc33aaaa3a..288f5ad92ac 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx @@ -1,14 +1,12 @@ import { render, waitFor, within, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { setupServer } from 'msw/node'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; -import { locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; +import { locationService, setDataSourceSrv } from '@grafana/runtime'; import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks'; -import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; import store from 'app/core/store'; import { @@ -19,6 +17,7 @@ import { import { AccessControlAction, ContactPointsState } from 'app/types'; import 'whatwg-fetch'; +import 'core-js/stable/structured-clone'; import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from '../../api/alertmanager'; import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi'; @@ -26,9 +25,11 @@ import { discoverAlertmanagerFeatures } from '../../api/buildInfo'; import { fetchNotifiers } from '../../api/grafana'; import * as receiversApi from '../../api/receiversApi'; import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp'; +import { mockApi, setupMswServer } from '../../mockApi'; import { mockDataSource, MockDataSourceSrv, + onCallPluginMetaMock, someCloudAlertManagerConfig, someCloudAlertManagerStatus, someGrafanaAlertManagerConfig, @@ -150,21 +151,16 @@ const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker'); +const server = setupMswServer(); + describe('Receivers', () => { - const server = setupServer(); - - beforeAll(() => { - setBackendSrv(backendSrv); - server.listen({ onUnhandledRequest: 'error' }); - }); - - afterAll(() => { - server.close(); - }); - beforeEach(() => { server.resetHandlers(); jest.resetAllMocks(); + + mockApi(server).grafanaNotifiers(grafanaNotifiersMock); + mockApi(server).plugins.getPluginSettings(onCallPluginMetaMock); + useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx index 0198aff5eac..f8b7ed01eb5 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx @@ -2,6 +2,7 @@ import { screen, render, within } from '@testing-library/react'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; +import { setBackendSrv } from '@grafana/runtime'; import { AlertManagerCortexConfig, GrafanaManagedReceiverConfig, @@ -10,7 +11,7 @@ import { import { configureStore } from 'app/store/configureStore'; import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } from 'app/types'; -import * as onCallApi from '../../api/onCallApi'; +import { backendSrv } from '../../../../../core/services/backend_srv'; import * as receiversApi from '../../api/receiversApi'; import { enableRBAC, grantUserPermissions } from '../../mocks'; import { fetchGrafanaNotifiersAction } from '../../state/actions'; @@ -18,7 +19,8 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { createUrl } from '../../utils/url'; import { ReceiversTable } from './ReceiversTable'; -import * as grafanaApp from './grafanaAppReceivers/grafanaApp'; +import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata'; +import { ReceiverMetadata } from './grafanaAppReceivers/useReceiversMetadata'; const renderReceieversTable = async ( receivers: Receiver[], @@ -58,16 +60,17 @@ const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({ options: [], }); -jest.spyOn(onCallApi, 'useGetOnCallIntegrationsQuery'); -const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker'); +const useReceiversMetadata = jest.spyOn(receiversMeta, 'useReceiversMetadata'); const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPointsState'); +setBackendSrv(backendSrv); + describe('ReceiversTable', () => { beforeEach(() => { jest.resetAllMocks(); const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState); - useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined); + useReceiversMetadata.mockReturnValue(new Map()); }); it('render receivers with grafana notifiers', async () => { diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index 73a70694934..9f1e8e5b173 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -26,9 +26,8 @@ import { ProvisioningBadge } from '../Provisioning'; import { ActionIcon } from '../rules/ActionIcon'; import { ReceiversSection } from './ReceiversSection'; -import { GrafanaAppBadge } from './grafanaAppReceivers/GrafanaAppBadge'; -import { useGetReceiversWithGrafanaAppTypes } from './grafanaAppReceivers/grafanaApp'; -import { ReceiverWithTypes } from './grafanaAppReceivers/types'; +import { ReceiverMetadataBadge } from './grafanaAppReceivers/ReceiverMetadataBadge'; +import { ReceiverMetadata, useReceiversMetadata } from './grafanaAppReceivers/useReceiversMetadata'; import { AlertmanagerConfigHealth, useAlertmanagerConfigHealth } from './useAlertmanagerConfigHealth'; interface UpdateActionProps extends ActionProps { @@ -183,6 +182,7 @@ interface ReceiverItem { types: string[]; provisioned?: boolean; grafanaAppReceiverType?: SupportedPlugin; + metadata?: ReceiverMetadata; } interface NotifierStatus { @@ -299,6 +299,7 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => { const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config); const { contactPointsState, errorStateAvailable } = useContactPointsState(alertManagerName); + const receiversMetadata = useReceiversMetadata(config.alertmanager_config.receivers ?? []); // receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted const [receiverToDelete, setReceiverToDelete] = useState(); @@ -325,10 +326,11 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => { setReceiverToDelete(undefined); }; - const receivers = useGetReceiversWithGrafanaAppTypes(config.alertmanager_config.receivers ?? []); const rows: RowItemTableProps[] = useMemo(() => { + const receivers = config.alertmanager_config.receivers ?? []; + return ( - receivers?.map((receiver: ReceiverWithTypes) => ({ + receivers.map((receiver) => ({ id: receiver.name, data: { name: receiver.name, @@ -340,12 +342,12 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => { return type; } ), - grafanaAppReceiverType: receiver.grafanaAppReceiverType, provisioned: receiver.grafana_managed_receiver_configs?.some((receiver) => receiver.provenance), + metadata: receiversMetadata.get(receiver), }, })) ?? [] ); - }, [grafanaNotifiers.result, receivers]); + }, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]); const columns = useGetColumns( alertManagerName, @@ -472,8 +474,8 @@ function useGetColumns( { id: 'type', label: 'Type', - renderCell: ({ data: { types, grafanaAppReceiverType } }) => ( - <>{grafanaAppReceiverType ? : types.join(', ')} + renderCell: ({ data: { types, metadata } }) => ( + <>{metadata ? : types.join(', ')} ), size: 2, }, diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index 9cdfc9747ea..c0a2ef50d79 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -17,6 +17,8 @@ export interface Props { errors?: FieldErrors; pathPrefix?: string; readOnly?: boolean; + + customValidators?: Record['customValidator']>; } export function ChannelOptions({ @@ -27,6 +29,7 @@ export function ChannelOptions({ errors, pathPrefix = '', readOnly = false, + customValidators = {}, }: Props): JSX.Element { const { watch } = useFormContext>(); const currentFormValues = watch(); // react hook form types ARE LYING! @@ -78,6 +81,7 @@ export function ChannelOptions({ pathPrefix={pathPrefix} pathSuffix={option.secure ? 'secureSettings.' : 'settings.'} option={option} + customValidator={customValidators[option.propertyName]} /> ); })} 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 7d967e247e3..77691f9ecbb 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -1,21 +1,24 @@ import { css } from '@emotion/css'; -import React, { useEffect, useMemo, useState } from 'react'; +import { sortBy } from 'lodash'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useFormContext, FieldErrors, FieldValues } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui'; -import { NotifierDTO } from 'app/types'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form'; +import { OnCallIntegrationType } from '../grafanaAppReceivers/onCall/useOnCallIntegration'; import { ChannelOptions } from './ChannelOptions'; import { CollapsibleSection } from './CollapsibleSection'; +import { Notifier } from './notifiers'; interface Props { defaultValues: R; + initialValues?: R; pathPrefix: string; - notifiers: NotifierDTO[]; + notifiers: Notifier[]; onDuplicate: () => void; onTest?: () => void; commonSettingsComponent: CommonSettingsComponentType; @@ -25,10 +28,13 @@ interface Props { onDelete?: () => void; isEditable?: boolean; isTestable?: boolean; + + customValidators?: React.ComponentProps['customValidators']; } export function ChannelSubForm({ defaultValues, + initialValues, pathPrefix, onDuplicate, onDelete, @@ -39,13 +45,20 @@ export function ChannelSubForm({ commonSettingsComponent: CommonSettingsComponent, isEditable = true, isTestable, + customValidators = {}, }: Props): JSX.Element { const styles = useStyles2(getStyles); - const name = (fieldName: string) => `${pathPrefix}${fieldName}`; + + const fieldName = useCallback((fieldName: string) => `${pathPrefix}${fieldName}`, [pathPrefix]); + const { control, watch, register, trigger, formState, setValue } = useFormContext(); - const selectedType = watch(name('type')) ?? defaultValues.type; // nope, setting "default" does not work at all. + const selectedType = watch(fieldName('type')) ?? defaultValues.type; // nope, setting "default" does not work at all. const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers); + // TODO I don't like integration specific code here but other ways require a bigger refactoring + const onCallIntegrationType = watch(fieldName('settings.integration_type')); + const isTestAvailable = onCallIntegrationType !== OnCallIntegrationType.NewIntegration; + useEffect(() => { register(`${pathPrefix}.__id`); /* Need to manually register secureFields or else they'll @@ -53,6 +66,26 @@ export function ChannelSubForm({ register(`${pathPrefix}.secureFields`); }, [register, pathPrefix]); + // Prevent forgetting about initial values when switching the integration type and the oncall integration type + useEffect(() => { + // Restore values when switching back from a changed integration to the default one + const subscription = watch((_, { name, type, value }) => { + if (initialValues && name === fieldName('type') && value === initialValues.type && type === 'change') { + setValue(fieldName('settings'), initialValues.settings); + } + // Restore initial value of an existing oncall integration + if ( + initialValues && + name === fieldName('settings.integration_type') && + value === OnCallIntegrationType.ExistingIntegration + ) { + setValue(fieldName('settings.url'), initialValues.settings['url']); + } + }); + + return () => subscription.unsubscribe(); + }, [selectedType, initialValues, setValue, fieldName, watch]); + const [_secureFields, setSecureFields] = useState(secureFields ?? {}); const onResetSecureField = (key: string) => { @@ -66,12 +99,15 @@ export function ChannelSubForm({ const typeOptions = useMemo( (): SelectableValue[] => - notifiers - .map(({ name, type }) => ({ + sortBy(notifiers, ({ dto, meta }) => [meta?.order ?? 0, dto.name]) + // .notifiers.sort((a, b) => a.dto.name.localeCompare(b.dto.name)) + .map(({ dto: { name, type }, meta }) => ({ label: name, value: type, - })) - .sort((a, b) => a.label.localeCompare(b.label)), + description: meta?.description, + isDisabled: meta ? !meta.enabled : false, + imgUrl: meta?.iconUrl, + })), [notifiers] ); @@ -84,11 +120,11 @@ export function ChannelSubForm({ } }; - const notifier = notifiers.find(({ type }) => type === selectedType); + const notifier = notifiers.find(({ dto: { type } }) => type === selectedType); // if there are mandatory options defined, optional options will be hidden by a collapse // if there aren't mandatory options, all options will be shown without collapse - const mandatoryOptions = notifier?.options.filter((o) => o.required); - const optionalOptions = notifier?.options.filter((o) => !o.required); + const mandatoryOptions = notifier?.dto.options.filter((o) => o.required); + const optionalOptions = notifier?.dto.options.filter((o) => !o.required); const contactPointTypeInputId = `contact-point-type-${pathPrefix}`; @@ -98,7 +134,7 @@ export function ChannelSubForm({
( onChange(value.value)} + {...field} /> )} control={control} name={name} + defaultValue={option.defaultValue} + rules={{ + validate: { + customValidator: (v) => (customValidator ? customValidator(v) : true), + }, + }} /> ); - + case 'radio': + return ( + <> + {option.label} + ( + + )} + control={control} + defaultValue={option.defaultValue?.value} + name={name} + rules={{ + required: option.required ? 'Option is required' : false, + validate: { + validationRule: (v) => (option.validationRule ? validateOption(v, option.validationRule) : true), + customValidator: (v) => (customValidator ? customValidator(v) : true), + }, + }} + /> + + ); case 'textarea': return (