mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: miscellaneous alertmanager config issues, tests (#35364)
This commit is contained in:
parent
673b03671d
commit
0439009d4f
@ -3,15 +3,16 @@ import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { Route } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||||
import { byTestId } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
import AmRoutes from './AmRoutes';
|
import AmRoutes from './AmRoutes';
|
||||||
import { fetchAlertManagerConfig } from './api/alertmanager';
|
import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager';
|
||||||
import { mockDataSource, MockDataSourceSrv } from './mocks';
|
import { mockDataSource, MockDataSourceSrv } from './mocks';
|
||||||
import { getAllDataSources } from './utils/config';
|
import { getAllDataSources } from './utils/config';
|
||||||
import { DataSourceType } from './utils/datasource';
|
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
@ -35,6 +36,7 @@ const mocks = {
|
|||||||
|
|
||||||
api: {
|
api: {
|
||||||
fetchAlertManagerConfig: typeAsJestMock(fetchAlertManagerConfig),
|
fetchAlertManagerConfig: typeAsJestMock(fetchAlertManagerConfig),
|
||||||
|
updateAlertManagerConfig: typeAsJestMock(updateAlertManagerConfig),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,6 +64,18 @@ const ui = {
|
|||||||
rootGroupBy: byTestId('am-routes-root-group-by'),
|
rootGroupBy: byTestId('am-routes-root-group-by'),
|
||||||
rootTimings: byTestId('am-routes-root-timings'),
|
rootTimings: byTestId('am-routes-root-timings'),
|
||||||
row: byTestId('am-routes-row'),
|
row: byTestId('am-routes-row'),
|
||||||
|
|
||||||
|
rootRouteContainer: byTestId('am-root-route-container'),
|
||||||
|
|
||||||
|
editButton: byRole('button', { name: 'Edit' }),
|
||||||
|
saveButton: byRole('button', { name: 'Save' }),
|
||||||
|
|
||||||
|
receiverSelect: byTestId('am-receiver-select'),
|
||||||
|
groupSelect: byTestId('am-group-select'),
|
||||||
|
|
||||||
|
groupWaitContainer: byTestId('am-group-wait'),
|
||||||
|
groupIntervalContainer: byTestId('am-group-interval'),
|
||||||
|
groupRepeatContainer: byTestId('am-repeat-interval'),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AmRoutes', () => {
|
describe('AmRoutes', () => {
|
||||||
@ -130,11 +144,19 @@ describe('AmRoutes', () => {
|
|||||||
routes: subroutes,
|
routes: subroutes,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
|
});
|
||||||
|
|
||||||
mocks.api.fetchAlertManagerConfig.mockImplementation(() =>
|
afterEach(() => {
|
||||||
Promise.resolve({
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
setDataSourceSrv(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads and shows routes', async () => {
|
||||||
|
mocks.api.fetchAlertManagerConfig.mockResolvedValue({
|
||||||
alertmanager_config: {
|
alertmanager_config: {
|
||||||
route: rootRoute,
|
route: rootRoute,
|
||||||
receivers: [
|
receivers: [
|
||||||
@ -150,21 +172,8 @@ describe('AmRoutes', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
template_files: {},
|
template_files: {},
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
|
|
||||||
setDataSourceSrv(undefined as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads and shows routes', async () => {
|
|
||||||
await renderAmRoutes();
|
await renderAmRoutes();
|
||||||
|
|
||||||
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
|
||||||
@ -197,4 +206,150 @@ describe('AmRoutes', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can edit root route if one is already defined', async () => {
|
||||||
|
const defaultConfig: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'default' }, { name: 'critical' }],
|
||||||
|
route: {
|
||||||
|
receiver: 'default',
|
||||||
|
group_by: ['alertname'],
|
||||||
|
},
|
||||||
|
templates: [],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
const currentConfig = { current: defaultConfig };
|
||||||
|
mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => {
|
||||||
|
currentConfig.current = newConfig;
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
|
||||||
|
return Promise.resolve(currentConfig.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderAmRoutes();
|
||||||
|
expect(await ui.rootReceiver.find()).toHaveTextContent('default');
|
||||||
|
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname');
|
||||||
|
|
||||||
|
// open root route for editing
|
||||||
|
const rootRouteContainer = await ui.rootRouteContainer.find();
|
||||||
|
userEvent.click(ui.editButton.get(rootRouteContainer));
|
||||||
|
|
||||||
|
// configure receiver & group by
|
||||||
|
const receiverSelect = await ui.receiverSelect.find();
|
||||||
|
clickSelectOption(receiverSelect, 'critical');
|
||||||
|
|
||||||
|
const groupSelect = ui.groupSelect.get();
|
||||||
|
await userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}');
|
||||||
|
|
||||||
|
// configure timing intervals
|
||||||
|
userEvent.click(byText('Timing options').get(rootRouteContainer));
|
||||||
|
|
||||||
|
await updateTiming(ui.groupWaitContainer.get(), '1', 'Minutes');
|
||||||
|
await updateTiming(ui.groupIntervalContainer.get(), '4', 'Minutes');
|
||||||
|
await updateTiming(ui.groupRepeatContainer.get(), '5', 'Hours');
|
||||||
|
|
||||||
|
//save
|
||||||
|
userEvent.click(ui.saveButton.get(rootRouteContainer));
|
||||||
|
|
||||||
|
// wait for it to go out of edit mode
|
||||||
|
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
|
||||||
|
|
||||||
|
// check that appropriate api calls were made
|
||||||
|
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'default' }, { name: 'critical' }],
|
||||||
|
route: {
|
||||||
|
continue: false,
|
||||||
|
group_by: ['alertname', 'namespace'],
|
||||||
|
receiver: 'critical',
|
||||||
|
routes: [],
|
||||||
|
group_interval: '4m',
|
||||||
|
group_wait: '1m',
|
||||||
|
repeat_interval: '5h',
|
||||||
|
},
|
||||||
|
templates: [],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// check that new config values are rendered
|
||||||
|
await waitFor(() => expect(ui.rootReceiver.query()).toHaveTextContent('critical'));
|
||||||
|
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname, namespace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit root route if one is not defined yet', async () => {
|
||||||
|
mocks.api.fetchAlertManagerConfig.mockResolvedValue({
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'default' }],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderAmRoutes();
|
||||||
|
|
||||||
|
// open root route for editing
|
||||||
|
const rootRouteContainer = await ui.rootRouteContainer.find();
|
||||||
|
userEvent.click(ui.editButton.get(rootRouteContainer));
|
||||||
|
|
||||||
|
// configure receiver & group by
|
||||||
|
const receiverSelect = await ui.receiverSelect.find();
|
||||||
|
clickSelectOption(receiverSelect, 'default');
|
||||||
|
|
||||||
|
const groupSelect = ui.groupSelect.get();
|
||||||
|
await userEvent.type(byRole('textbox').get(groupSelect), 'severity{enter}');
|
||||||
|
await userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}');
|
||||||
|
//save
|
||||||
|
userEvent.click(ui.saveButton.get(rootRouteContainer));
|
||||||
|
|
||||||
|
// wait for it to go out of edit mode
|
||||||
|
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
|
||||||
|
|
||||||
|
// check that appropriate api calls were made
|
||||||
|
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'default' }],
|
||||||
|
route: {
|
||||||
|
continue: false,
|
||||||
|
group_by: ['severity', 'namespace'],
|
||||||
|
receiver: 'default',
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show error message if loading Alermanager config fails', async () => {
|
||||||
|
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
|
||||||
|
status: 500,
|
||||||
|
data: {
|
||||||
|
message: "Alertmanager has exploded. it's gone. Forget about it.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await renderAmRoutes();
|
||||||
|
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
|
||||||
|
expect(await byText("Alertmanager has exploded. it's gone. Forget about it.").find()).toBeInTheDocument();
|
||||||
|
expect(ui.rootReceiver.query()).not.toBeInTheDocument();
|
||||||
|
expect(ui.editButton.query()).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
|
||||||
|
userEvent.click(byRole('textbox').get(selectElement));
|
||||||
|
userEvent.click(byText(optionText).get(selectElement));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise<void> => {
|
||||||
|
const inputs = byRole('textbox').queryAll(selectElement);
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
await userEvent.type(inputs[0], value);
|
||||||
|
userEvent.click(inputs[1]);
|
||||||
|
userEvent.click(byText(timeUnit).get(selectElement));
|
||||||
|
};
|
||||||
|
@ -39,7 +39,7 @@ const AmRoutes: FC = () => {
|
|||||||
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
|
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
|
||||||
|
|
||||||
const config = result?.alertmanager_config;
|
const config = result?.alertmanager_config;
|
||||||
const [routes, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
|
const [rootRoute, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
|
||||||
|
|
||||||
const receivers = stringsToSelectableValues(
|
const receivers = stringsToSelectableValues(
|
||||||
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
|
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
|
||||||
@ -54,14 +54,10 @@ const AmRoutes: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useCleanup((state) => state.unifiedAlerting.saveAMConfig);
|
useCleanup((state) => state.unifiedAlerting.saveAMConfig);
|
||||||
const { loading: saving, error: savingError, dispatched: savingDispatched } = useUnifiedAlertingSelector(
|
|
||||||
(state) => state.saveAMConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSave = (data: Partial<FormAmRoute>) => {
|
const handleSave = (data: Partial<FormAmRoute>) => {
|
||||||
const newData = formAmRouteToAmRoute(
|
const newData = formAmRouteToAmRoute(
|
||||||
{
|
{
|
||||||
...routes,
|
...rootRoute,
|
||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
id2ExistingRoute
|
id2ExistingRoute
|
||||||
@ -83,16 +79,11 @@ const AmRoutes: FC = () => {
|
|||||||
oldConfig: result,
|
oldConfig: result,
|
||||||
alertManagerSourceName: alertManagerSourceName!,
|
alertManagerSourceName: alertManagerSourceName!,
|
||||||
successMessage: 'Saved',
|
successMessage: 'Saved',
|
||||||
|
refetch: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (savingDispatched && !saving && !savingError) {
|
|
||||||
fetchConfig();
|
|
||||||
}
|
|
||||||
}, [fetchConfig, savingDispatched, saving, savingError]);
|
|
||||||
|
|
||||||
if (!alertManagerSourceName) {
|
if (!alertManagerSourceName) {
|
||||||
return <Redirect to="/alerting/routes" />;
|
return <Redirect to="/alerting/routes" />;
|
||||||
}
|
}
|
||||||
@ -115,14 +106,14 @@ const AmRoutes: FC = () => {
|
|||||||
onEnterEditMode={enterRootRouteEditMode}
|
onEnterEditMode={enterRootRouteEditMode}
|
||||||
onExitEditMode={exitRootRouteEditMode}
|
onExitEditMode={exitRootRouteEditMode}
|
||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={routes}
|
routes={rootRoute}
|
||||||
/>
|
/>
|
||||||
<div className={styles.break} />
|
<div className={styles.break} />
|
||||||
<AmSpecificRouting
|
<AmSpecificRouting
|
||||||
onChange={handleSave}
|
onChange={handleSave}
|
||||||
onRootRouteEdit={enterRootRouteEditMode}
|
onRootRouteEdit={enterRootRouteEditMode}
|
||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={routes}
|
routes={rootRoute}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -28,8 +28,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P
|
|||||||
// if no config has been uploaded to grafana, it returns error instead of latest config
|
// if no config has been uploaded to grafana, it returns error instead of latest config
|
||||||
if (
|
if (
|
||||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
|
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
|
||||||
(e.data?.message?.includes('failed to get latest configuration') ||
|
e.data?.message?.includes('could not find an Alertmanager configuration')
|
||||||
e.data?.message?.includes('could not find an Alertmanager configuration'))
|
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
template_files: {},
|
template_files: {},
|
||||||
|
@ -28,7 +28,7 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container} data-testid="am-root-route-container">
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
<h5 className={styles.title}>
|
<h5 className={styles.title}>
|
||||||
Root policy - <i>default for all alerts</i>
|
Root policy - <i>default for all alerts</i>
|
||||||
|
@ -36,8 +36,9 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
<Form defaultValues={routes} onSubmit={onSave}>
|
<Form defaultValues={routes} onSubmit={onSave}>
|
||||||
{({ control, errors, setValue }) => (
|
{({ control, errors, setValue }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="Default contact point">
|
<Field label="Default contact point" invalid={!!errors.receiver} error={errors.receiver?.message}>
|
||||||
<div className={styles.container}>
|
<>
|
||||||
|
<div className={styles.container} data-testid="am-receiver-select">
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
<Select
|
<Select
|
||||||
@ -49,14 +50,20 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="receiver"
|
name="receiver"
|
||||||
|
rules={{ required: { value: true, message: 'Required.' } }}
|
||||||
/>
|
/>
|
||||||
<span>or</span>
|
<span>or</span>
|
||||||
<Link href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}>
|
<Link href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}>
|
||||||
Create a contact point
|
Create a contact point
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Group by" description="Group alerts when you receive a notification based on labels.">
|
<Field
|
||||||
|
label="Group by"
|
||||||
|
description="Group alerts when you receive a notification based on labels."
|
||||||
|
data-testid="am-group-select"
|
||||||
|
>
|
||||||
{/* @ts-ignore-check: react-hook-form made me do this */}
|
{/* @ts-ignore-check: react-hook-form made me do this */}
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { onChange, ref, ...field } }) => (
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
@ -86,9 +93,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
label="Group wait"
|
label="Group wait"
|
||||||
description="The waiting time until the initial notification is sent for a new group created by an incoming alert."
|
description="The waiting time until the initial notification is sent for a new group created by an incoming alert. Default 30 seconds."
|
||||||
invalid={!!errors.groupWaitValue}
|
invalid={!!errors.groupWaitValue}
|
||||||
error={errors.groupWaitValue?.message}
|
error={errors.groupWaitValue?.message}
|
||||||
|
data-testid="am-group-wait"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={cx(styles.container, styles.timingContainer)}>
|
<div className={cx(styles.container, styles.timingContainer)}>
|
||||||
@ -119,9 +127,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Group interval"
|
label="Group interval"
|
||||||
description="The waiting time to send a batch of new alerts for that group after the first notification was sent."
|
description="The waiting time to send a batch of new alerts for that group after the first notification was sent. Default 5 minutes."
|
||||||
invalid={!!errors.groupIntervalValue}
|
invalid={!!errors.groupIntervalValue}
|
||||||
error={errors.groupIntervalValue?.message}
|
error={errors.groupIntervalValue?.message}
|
||||||
|
data-testid="am-group-interval"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={cx(styles.container, styles.timingContainer)}>
|
<div className={cx(styles.container, styles.timingContainer)}>
|
||||||
@ -152,9 +161,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
|||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Repeat interval"
|
label="Repeat interval"
|
||||||
description="The waiting time to resend an alert after they have successfully been sent."
|
description="The waiting time to resend an alert after they have successfully been sent. Default 4 hours."
|
||||||
invalid={!!errors.repeatIntervalValue}
|
invalid={!!errors.repeatIntervalValue}
|
||||||
error={errors.repeatIntervalValue?.message}
|
error={errors.repeatIntervalValue?.message}
|
||||||
|
data-testid="am-repeat-interval"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={cx(styles.container, styles.timingContainer)}>
|
<div className={cx(styles.container, styles.timingContainer)}>
|
||||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
||||||
import { emptyRoute } from '../../utils/amroutes';
|
import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
|
||||||
import { EmptyArea } from '../EmptyArea';
|
import { EmptyArea } from '../EmptyArea';
|
||||||
import { AmRoutesTable } from './AmRoutesTable';
|
import { AmRoutesTable } from './AmRoutesTable';
|
||||||
|
|
||||||
@ -22,7 +22,13 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({ onChange, onRoot
|
|||||||
|
|
||||||
const addNewRoute = () => {
|
const addNewRoute = () => {
|
||||||
setIsAddMode(true);
|
setIsAddMode(true);
|
||||||
setActualRoutes((actualRoutes) => [...actualRoutes, emptyRoute]);
|
setActualRoutes((actualRoutes) => [
|
||||||
|
...actualRoutes,
|
||||||
|
{
|
||||||
|
...emptyRoute,
|
||||||
|
matchers: [emptyArrayFieldMatcher],
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,6 +4,7 @@ import { Matcher, Route } from 'app/plugins/datasource/alertmanager/types';
|
|||||||
import { FormAmRoute } from '../types/amroutes';
|
import { FormAmRoute } from '../types/amroutes';
|
||||||
import { parseInterval, timeOptions } from './time';
|
import { parseInterval, timeOptions } from './time';
|
||||||
import { parseMatcher, stringifyMatcher } from './alertmanager';
|
import { parseMatcher, stringifyMatcher } from './alertmanager';
|
||||||
|
import { isUndefined, omitBy } from 'lodash';
|
||||||
|
|
||||||
const defaultValueAndType: [string, string] = ['', timeOptions[0].value];
|
const defaultValueAndType: [string, string] = ['', timeOptions[0].value];
|
||||||
|
|
||||||
@ -51,8 +52,8 @@ export const emptyArrayFieldMatcher: Matcher = {
|
|||||||
|
|
||||||
export const emptyRoute: FormAmRoute = {
|
export const emptyRoute: FormAmRoute = {
|
||||||
id: '',
|
id: '',
|
||||||
matchers: [emptyArrayFieldMatcher],
|
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
|
matchers: [],
|
||||||
routes: [],
|
routes: [],
|
||||||
continue: false,
|
continue: false,
|
||||||
receiver: '',
|
receiver: '',
|
||||||
@ -133,7 +134,7 @@ export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute, id2ExistingRoute:
|
|||||||
amRoute.receiver = formAmRoute.receiver;
|
amRoute.receiver = formAmRoute.receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
return amRoute;
|
return omitBy(amRoute, isUndefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stringToSelectableValue = (str: string): SelectableValue<string> => ({
|
export const stringToSelectableValue = (str: string): SelectableValue<string> => ({
|
||||||
|
@ -143,6 +143,8 @@ function messageFromError(e: Error | FetchError | SerializedError): string {
|
|||||||
.map((d) => d?.message)
|
.map((d) => d?.message)
|
||||||
.filter((m) => !!m)
|
.filter((m) => !!m)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
} else if (e.statusText) {
|
||||||
|
return e.statusText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (e as Error)?.message || String(e);
|
return (e as Error)?.message || String(e);
|
||||||
|
Loading…
Reference in New Issue
Block a user