mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 07:35:45 -06:00
Alerting: Add external Alertmanagers (#39183)
* building ui * saving alertmanager urls * add actions and api call to get external ams * add list to add modal * add validation and edit/delete * work on merging results * merging results * get color for status heart * adding tests * tests added * rename * add pollin and status * fix list sync * fix polling * add info icon with actual tooltip * fix test * Accessibility things * fix strict error * delete public/dist files * Add API tests for invalid URL * start redo admin test * Fix for empty configuration and test * remove admin test * text updates after review * suppress appevent error * fix tests * update description Co-authored-by: gotjosh <josue@grafana.com> * fix text plus go lint * updates after pr review * Adding docs * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * Update docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md Co-authored-by: gotjosh <josue@grafana.com> * prettier * updates after docs feedback Co-authored-by: gotjosh <josue.abreu@gmail.com> Co-authored-by: gotjosh <josue@grafana.com>
This commit is contained in:
parent
862054918d
commit
b2d7162168
@ -15,3 +15,25 @@ Grafana includes built-in support for Prometheus Alertmanager. By default, notif
|
||||
Grafana 8 alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Cortex or Loki data sources.
|
||||
|
||||
{{< figure max-width="40%" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" max-width="250px" caption="Select Alertmanager" >}}
|
||||
|
||||
You can configure one or several external Alertmanagers to receive alerts from Grafana. Once configured, both the embedded Alertmanager **and** any configured external Alertmanagers will receive _all_ alerts.
|
||||
|
||||
You can do the setup in the "Admin" tab within the Grafana v8 Alerts UI.
|
||||
|
||||
### Add a new external Alertmanager
|
||||
|
||||
1. In the Grafana menu, click the Alerting (bell) icon to open the Alerting page listing existing alerts.
|
||||
2. Click **Admin** and then scroll down to the External Alertmanager section.
|
||||
3. Click **Add Alertmanager** and a modal opens.
|
||||
4. Add the URL and the port for the external Alertmanager. You do not need to specify the path suffix, for example, `/api/v(1|2)/alerts`. Grafana automatically adds this.
|
||||
|
||||
The external URL is listed in the table with a pending status. Once Grafana verifies that the Alertmanager is discovered, the status changes to active. No requests are made to the external Alertmanager at this point; the verification signals that alerts are ready to be sent.
|
||||
|
||||
### Edit an external Alertmanager
|
||||
|
||||
1. Click the pen symbol to the right of the Alertmanager row in the table.
|
||||
2. When the edit modal opens, you can view all the URLs that were added.
|
||||
|
||||
The edited URL will be pending until Grafana verifies it again.
|
||||
|
||||
{{< figure max-width="40%" src="/static/img/docs/alerting/unified/ext-alertmanager-active.png" max-width="650px" caption="External Alertmanagers" >}}
|
||||
|
@ -70,6 +70,12 @@ func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.
|
||||
OrgID: c.OrgId,
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
msg := "failed to validate admin configuration"
|
||||
srv.log.Error(msg, "err", err)
|
||||
return ErrResp(http.StatusBadRequest, err, msg)
|
||||
}
|
||||
|
||||
cmd := store.UpdateAdminConfigurationCmd{AdminConfiguration: cfg}
|
||||
if err := srv.store.UpdateAdminConfiguration(cmd); err != nil {
|
||||
msg := "failed to save the admin configuration to the database"
|
||||
|
@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// AdminConfiguration represents the ngalert administration configuration settings.
|
||||
@ -22,3 +23,14 @@ func (ac *AdminConfiguration) AsSHA256() string {
|
||||
_, _ = h.Write([]byte(fmt.Sprintf("%v", ac.Alertmanagers)))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func (ac *AdminConfiguration) Validate() error {
|
||||
for _, u := range ac.Alertmanagers {
|
||||
_, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
58
pkg/services/ngalert/models/admin_configuration_test.go
Normal file
58
pkg/services/ngalert/models/admin_configuration_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminConfiguration_AsSHA256(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
ac *AdminConfiguration
|
||||
ciphertext string
|
||||
}{
|
||||
{
|
||||
name: "AsSHA256",
|
||||
ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093"}},
|
||||
ciphertext: "3ec9db375a5ba12f7c7b704922cf4b8e21a31e30d85be2386803829f0ee24410",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.ciphertext, tt.ac.AsSHA256())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminConfiguration_Validate(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
ac *AdminConfiguration
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "should return the first error if any of the Alertmanagers URL is invalid",
|
||||
ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093", "http://›∂-)Æÿ ñ"}},
|
||||
err: fmt.Errorf("parse \"http://›∂-)Æÿ ñ\": invalid character \" \" in host name"),
|
||||
},
|
||||
{
|
||||
name: "should not return any errors if all URLs are valid",
|
||||
ac: &AdminConfiguration{Alertmanagers: []string{"http://localhost:9093"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.ac.Validate()
|
||||
if tt.err != nil {
|
||||
require.EqualError(t, err, tt.err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,150 +1,13 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form } from '@grafana/ui';
|
||||
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||
import React from 'react';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from './utils/datasource';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
deleteAlertManagerConfigAction,
|
||||
fetchAlertManagerConfigAction,
|
||||
updateAlertManagerConfigAction,
|
||||
} from './state/actions';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
|
||||
interface FormValues {
|
||||
configJSON: string;
|
||||
}
|
||||
import AlertmanagerConfig from './components/admin/AlertmanagerConfig';
|
||||
import { ExternalAlertmanagers } from './components/admin/ExternalAlertmanagers';
|
||||
|
||||
export default function Admin(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||
const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false);
|
||||
const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig);
|
||||
const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
|
||||
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : false;
|
||||
|
||||
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const { result: config, loading: isLoadingConfig, error: loadingError } =
|
||||
(alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
|
||||
|
||||
useEffect(() => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||
}
|
||||
}, [alertManagerSourceName, dispatch]);
|
||||
|
||||
const resetConfig = () => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(deleteAlertManagerConfigAction(alertManagerSourceName));
|
||||
}
|
||||
setShowConfirmDeleteAMConfig(false);
|
||||
};
|
||||
|
||||
const defaultValues = useMemo(
|
||||
(): FormValues => ({
|
||||
configJSON: config ? JSON.stringify(config, null, 2) : '',
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
const loading = isDeleting || isLoadingConfig || isSaving;
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(
|
||||
updateAlertManagerConfigAction({
|
||||
newConfig: JSON.parse(values.configJSON),
|
||||
oldConfig: config,
|
||||
alertManagerSourceName,
|
||||
successMessage: 'Alertmanager configuration updated.',
|
||||
refetch: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="alerting-admin">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
{loadingError && !loading && (
|
||||
<Alert severity="error" title="Error loading Alertmanager configuration">
|
||||
{loadingError.message || 'Unknown error.'}
|
||||
</Alert>
|
||||
)}
|
||||
{isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && (
|
||||
<Alert severity="info" title="Resetting Alertmanager configuration">
|
||||
It might take a while...
|
||||
</Alert>
|
||||
)}
|
||||
{alertManagerSourceName && config && (
|
||||
<Form defaultValues={defaultValues} onSubmit={onSubmit} key={defaultValues.configJSON}>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
{!readOnly && (
|
||||
<Field
|
||||
disabled={loading}
|
||||
label="Configuration"
|
||||
invalid={!!errors.configJSON}
|
||||
error={errors.configJSON?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...register('configJSON', {
|
||||
required: { value: true, message: 'Required.' },
|
||||
validate: (v) => {
|
||||
try {
|
||||
JSON.parse(v);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
},
|
||||
})}
|
||||
id="configuration"
|
||||
rows={25}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{readOnly && (
|
||||
<Field label="Configuration">
|
||||
<pre data-testid="readonly-config">{defaultValues.configJSON}</pre>
|
||||
</Field>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<HorizontalGroup>
|
||||
<Button type="submit" variant="primary" disabled={loading}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
variant="destructive"
|
||||
onClick={() => setShowConfirmDeleteAMConfig(true)}
|
||||
>
|
||||
Reset configuration
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
{!!showConfirmDeleteAMConfig && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
title="Reset Alertmanager configuration"
|
||||
body={`Are you sure you want to reset configuration ${
|
||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME
|
||||
? 'for the Grafana Alertmanager'
|
||||
: `for "${alertManagerSourceName}"`
|
||||
}? Contact points and notification policies will be reset to their defaults.`}
|
||||
confirmText="Yes, reset configuration"
|
||||
onConfirm={resetConfig}
|
||||
onDismiss={() => setShowConfirmDeleteAMConfig(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
<AlertmanagerConfig test-id="admin-alertmanagerconfig" />
|
||||
<ExternalAlertmanagers test-id="admin-externalalertmanagers" />
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
TestReceiversPayload,
|
||||
TestReceiversResult,
|
||||
TestReceiversAlert,
|
||||
ExternalAlertmanagersResponse,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
@ -194,6 +195,43 @@ export async function testReceivers(
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAlertManagers(alertManagers: string[]): Promise<void> {
|
||||
await lastValueFrom(
|
||||
getBackendSrv().fetch({
|
||||
method: 'POST',
|
||||
data: { alertmanagers: alertManagers },
|
||||
url: '/api/v1/ngalert/admin_config',
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
).then(() => {
|
||||
fetchExternalAlertmanagerConfig();
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchExternalAlertmanagers(): Promise<ExternalAlertmanagersResponse> {
|
||||
const result = await lastValueFrom(
|
||||
getBackendSrv().fetch<ExternalAlertmanagersResponse>({
|
||||
method: 'GET',
|
||||
url: '/api/v1/ngalert/alertmanagers',
|
||||
})
|
||||
);
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function fetchExternalAlertmanagerConfig(): Promise<{ alertmanagers: string[] }> {
|
||||
const result = await lastValueFrom(
|
||||
getBackendSrv().fetch<{ alertmanagers: string[] }>({
|
||||
method: 'GET',
|
||||
url: '/api/v1/ngalert/admin_config',
|
||||
showErrorAlert: false,
|
||||
})
|
||||
);
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
function escapeQuotes(value: string): string {
|
||||
return value.replace(/"/g, '\\"');
|
||||
}
|
||||
|
@ -0,0 +1,138 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Field, FieldArray, Form, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { addExternalAlertmanagersAction } from '../../state/actions';
|
||||
import { AlertmanagerUrl } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
alertmanagers: AlertmanagerUrl[];
|
||||
}
|
||||
|
||||
export const AddAlertManagerModal: FC<Props> = ({ alertmanagers, onClose }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const defaultValues: Record<string, AlertmanagerUrl[]> = useMemo(
|
||||
() => ({
|
||||
alertmanagers: alertmanagers,
|
||||
}),
|
||||
[alertmanagers]
|
||||
);
|
||||
|
||||
const modalTitle = (
|
||||
<div className={styles.modalTitle}>
|
||||
<Icon name="bell" className={styles.modalIcon} />
|
||||
<h3>Add Alertmanager</h3>
|
||||
</div>
|
||||
);
|
||||
|
||||
const onSubmit = (values: Record<string, AlertmanagerUrl[]>) => {
|
||||
dispatch(addExternalAlertmanagersAction(values.alertmanagers.map((am) => cleanAlertmanagerUrl(am.url))));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={modalTitle} isOpen={true} onDismiss={onClose} className={styles.modal}>
|
||||
<div className={styles.description}>
|
||||
We use a service discovery method to find existing Alertmanagers for a given URL.
|
||||
</div>
|
||||
<Form onSubmit={onSubmit} defaultValues={defaultValues}>
|
||||
{({ register, control, errors }) => (
|
||||
<div>
|
||||
<FieldArray control={control} name="alertmanagers">
|
||||
{({ fields, append, remove }) => (
|
||||
<div className={styles.fieldArray}>
|
||||
<div className={styles.bold}>Source url</div>
|
||||
<div className={styles.muted}>
|
||||
Authentication can be done via URL (e.g. user:password@myalertmanager.com) and only the Alertmanager
|
||||
v2 API is supported. The suffix is added internally, there is no need to specify it.
|
||||
</div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Field
|
||||
invalid={!!errors?.alertmanagers?.[index]}
|
||||
error="Field is required"
|
||||
key={`${field.id}-${index}`}
|
||||
>
|
||||
<Input
|
||||
className={styles.input}
|
||||
defaultValue={field.url}
|
||||
{...register(`alertmanagers.${index}.url`, { required: true })}
|
||||
placeholder="http://localhost:9093"
|
||||
addonAfter={
|
||||
<Button
|
||||
aria-label="Remove alertmanager"
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
variant="destructive"
|
||||
className={styles.destroyInputRow}
|
||||
>
|
||||
<Icon name="trash-alt" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
<Button type="button" variant="secondary" onClick={() => append({ url: '' })}>
|
||||
Add URL
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FieldArray>
|
||||
<div>
|
||||
<Button onSubmit={() => onSubmit}>Add Alertmanagers</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
function cleanAlertmanagerUrl(url: string): string {
|
||||
return url.replace(/\/$/, '').replace(/\/api\/v[1|2]\/alerts/i, '');
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const muted = css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`;
|
||||
return {
|
||||
description: cx(
|
||||
css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
muted
|
||||
),
|
||||
muted: muted,
|
||||
bold: css`
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
`,
|
||||
modal: css``,
|
||||
modalIcon: cx(
|
||||
muted,
|
||||
css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`
|
||||
),
|
||||
modalTitle: css`
|
||||
display: flex;
|
||||
`,
|
||||
input: css`
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
inputRow: css`
|
||||
display: flex;
|
||||
`,
|
||||
destroyInputRow: css`
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
fieldArray: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
};
|
||||
};
|
@ -1,22 +1,27 @@
|
||||
import React from 'react';
|
||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||
import { getAllDataSources } from './utils/config';
|
||||
import { getAllDataSources } from '../../utils/config';
|
||||
import {
|
||||
fetchAlertManagerConfig,
|
||||
deleteAlertManagerConfig,
|
||||
updateAlertManagerConfig,
|
||||
fetchStatus,
|
||||
} from './api/alertmanager';
|
||||
} from '../../api/alertmanager';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import Admin from './Admin';
|
||||
import AlertmanagerConfig from './AlertmanagerConfig';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../../utils/constants';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
|
||||
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import {
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
someCloudAlertManagerConfig,
|
||||
someCloudAlertManagerStatus,
|
||||
} from '../../mocks';
|
||||
import { DataSourceType } from '../../utils/datasource';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import store from 'app/core/store';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@ -26,9 +31,9 @@ import {
|
||||
AlertManagerImplementation,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
jest.mock('./api/alertmanager');
|
||||
jest.mock('./api/grafana');
|
||||
jest.mock('./utils/config');
|
||||
jest.mock('../../api/alertmanager');
|
||||
jest.mock('../../api/grafana');
|
||||
jest.mock('../../utils/config');
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: typeAsJestMock(getAllDataSources),
|
||||
@ -52,7 +57,7 @@ const renderAdminPage = (alertManagerSourceName?: string) => {
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<Admin />
|
||||
<AlertmanagerConfig />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
@ -80,7 +85,7 @@ const ui = {
|
||||
readOnlyConfig: byTestId('readonly-config'),
|
||||
};
|
||||
|
||||
describe('Alerting Admin', () => {
|
||||
describe('Admin config', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form, useStyles2 } from '@grafana/ui';
|
||||
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
|
||||
import { AlertManagerPicker } from '../AlertManagerPicker';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
deleteAlertManagerConfigAction,
|
||||
fetchAlertManagerConfigAction,
|
||||
updateAlertManagerConfigAction,
|
||||
} from '../../state/actions';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
|
||||
interface FormValues {
|
||||
configJSON: string;
|
||||
}
|
||||
|
||||
export default function AlertmanagerConfig(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||
const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false);
|
||||
const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig);
|
||||
const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
|
||||
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : false;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const { result: config, loading: isLoadingConfig, error: loadingError } =
|
||||
(alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
|
||||
|
||||
useEffect(() => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||
}
|
||||
}, [alertManagerSourceName, dispatch]);
|
||||
|
||||
const resetConfig = () => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(deleteAlertManagerConfigAction(alertManagerSourceName));
|
||||
}
|
||||
setShowConfirmDeleteAMConfig(false);
|
||||
};
|
||||
|
||||
const defaultValues = useMemo(
|
||||
(): FormValues => ({
|
||||
configJSON: config ? JSON.stringify(config, null, 2) : '',
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
const loading = isDeleting || isLoadingConfig || isSaving;
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(
|
||||
updateAlertManagerConfigAction({
|
||||
newConfig: JSON.parse(values.configJSON),
|
||||
oldConfig: config,
|
||||
alertManagerSourceName,
|
||||
successMessage: 'Alertmanager configuration updated.',
|
||||
refetch: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
{loadingError && !loading && (
|
||||
<Alert severity="error" title="Error loading Alertmanager configuration">
|
||||
{loadingError.message || 'Unknown error.'}
|
||||
</Alert>
|
||||
)}
|
||||
{isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && (
|
||||
<Alert severity="info" title="Resetting Alertmanager configuration">
|
||||
It might take a while...
|
||||
</Alert>
|
||||
)}
|
||||
{alertManagerSourceName && config && (
|
||||
<Form defaultValues={defaultValues} onSubmit={onSubmit} key={defaultValues.configJSON}>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
{!readOnly && (
|
||||
<Field
|
||||
disabled={loading}
|
||||
label="Configuration"
|
||||
invalid={!!errors.configJSON}
|
||||
error={errors.configJSON?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...register('configJSON', {
|
||||
required: { value: true, message: 'Required.' },
|
||||
validate: (v) => {
|
||||
try {
|
||||
JSON.parse(v);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
},
|
||||
})}
|
||||
id="configuration"
|
||||
rows={25}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{readOnly && (
|
||||
<Field label="Configuration">
|
||||
<pre data-testid="readonly-config">{defaultValues.configJSON}</pre>
|
||||
</Field>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<HorizontalGroup>
|
||||
<Button type="submit" variant="primary" disabled={loading}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
variant="destructive"
|
||||
onClick={() => setShowConfirmDeleteAMConfig(true)}
|
||||
>
|
||||
Reset configuration
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
{!!showConfirmDeleteAMConfig && (
|
||||
<ConfirmModal
|
||||
isOpen={true}
|
||||
title="Reset Alertmanager configuration"
|
||||
body={`Are you sure you want to reset configuration ${
|
||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME
|
||||
? 'for the Grafana Alertmanager'
|
||||
: `for "${alertManagerSourceName}"`
|
||||
}? Contact points and notification policies will be reset to their defaults.`}
|
||||
confirmText="Yes, reset configuration"
|
||||
onConfirm={resetConfig}
|
||||
onDismiss={() => setShowConfirmDeleteAMConfig(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
});
|
@ -0,0 +1,180 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, ConfirmModal, HorizontalGroup, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { AddAlertManagerModal } from './AddAlertManagerModal';
|
||||
import {
|
||||
addExternalAlertmanagersAction,
|
||||
fetchExternalAlertmanagersAction,
|
||||
fetchExternalAlertmanagersConfigAction,
|
||||
} from '../../state/actions';
|
||||
import { useExternalAmSelector } from '../../hooks/useExternalAmSelector';
|
||||
|
||||
export const ExternalAlertmanagers = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const [modalState, setModalState] = useState({ open: false, payload: [{ url: '' }] });
|
||||
const [deleteModalState, setDeleteModalState] = useState({ open: false, index: 0 });
|
||||
const externalAlertManagers = useExternalAmSelector();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchExternalAlertmanagersAction());
|
||||
dispatch(fetchExternalAlertmanagersConfigAction());
|
||||
const interval = setInterval(() => dispatch(fetchExternalAlertmanagersAction()), 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(index: number) => {
|
||||
// to delete we need to filter the alertmanager from the list and repost
|
||||
const newList = (externalAlertManagers ?? [])
|
||||
.filter((am, i) => i !== index)
|
||||
.map((am) => {
|
||||
return am.url;
|
||||
});
|
||||
dispatch(addExternalAlertmanagersAction(newList));
|
||||
setDeleteModalState({ open: false, index: 0 });
|
||||
},
|
||||
[externalAlertManagers, dispatch]
|
||||
);
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
const ams = externalAlertManagers ? [...externalAlertManagers] : [{ url: '' }];
|
||||
setModalState((state) => ({
|
||||
...state,
|
||||
open: true,
|
||||
payload: ams,
|
||||
}));
|
||||
}, [setModalState, externalAlertManagers]);
|
||||
|
||||
const onOpenModal = useCallback(() => {
|
||||
setModalState((state) => {
|
||||
const ams = externalAlertManagers ? [...externalAlertManagers, { url: '' }] : [{ url: '' }];
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
payload: ams,
|
||||
};
|
||||
});
|
||||
}, [externalAlertManagers]);
|
||||
|
||||
const onCloseModal = useCallback(() => {
|
||||
setModalState((state) => ({
|
||||
...state,
|
||||
open: false,
|
||||
}));
|
||||
}, [setModalState]);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'green';
|
||||
|
||||
case 'pending':
|
||||
return 'yellow';
|
||||
|
||||
default:
|
||||
return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const noAlertmanagers = externalAlertManagers?.length === 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>External Alertmanagers</h4>
|
||||
<div className={styles.muted}>
|
||||
You can have your Grafana managed alerts be delivered to one or many external Alertmanager(s) in addition to the
|
||||
internal Alertmanager by specifying their URLs below.
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{!noAlertmanagers && (
|
||||
<Button type="button" onClick={onOpenModal}>
|
||||
Add Alertmanager
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{noAlertmanagers ? (
|
||||
<EmptyListCTA
|
||||
title="You have not added any external alertmanagers"
|
||||
onClick={onOpenModal}
|
||||
buttonTitle="Add Alertmanager"
|
||||
buttonIcon="bell-slash"
|
||||
/>
|
||||
) : (
|
||||
<table className="filter-table form-inline filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Url</th>
|
||||
<th>Status</th>
|
||||
<th style={{ width: '2%' }}>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{externalAlertManagers?.map((am, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<span className={styles.url}>{am.url}</span>
|
||||
{am.actualUrl ? (
|
||||
<Tooltip content={`Discovered ${am.actualUrl} from ${am.url}`} theme="info">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<Icon name="heart" style={{ color: getStatusColor(am.status) }} title={am.status} />
|
||||
</td>
|
||||
<td>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" type="button" onClick={onEdit} aria-label="Edit alertmanager">
|
||||
<Icon name="pen" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
aria-label="Remove alertmanager"
|
||||
type="button"
|
||||
onClick={() => setDeleteModalState({ open: true, index })}
|
||||
>
|
||||
<Icon name="trash-alt" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={deleteModalState.open}
|
||||
title="Remove Alertmanager"
|
||||
body="Are you sure you want to remove this Alertmanager"
|
||||
confirmText="Remove"
|
||||
onConfirm={() => onDelete(deleteModalState.index)}
|
||||
onDismiss={() => setDeleteModalState({ open: false, index: 0 })}
|
||||
/>
|
||||
{modalState.open && <AddAlertManagerModal onClose={onCloseModal} alertmanagers={modalState.payload} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
url: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
muted: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
actions: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
table: css``,
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
import { useExternalAmSelector } from './useExternalAmSelector';
|
||||
|
||||
const createMockStoreState = (
|
||||
activeAlertmanagers: Array<{ url: string }>,
|
||||
droppedAlertmanagers: Array<{ url: string }>,
|
||||
alertmanagerConfig: string[]
|
||||
) => ({
|
||||
unifiedAlerting: {
|
||||
externalAlertmanagers: {
|
||||
discoveredAlertmanagers: {
|
||||
result: {
|
||||
data: {
|
||||
activeAlertManagers: activeAlertmanagers,
|
||||
droppedAlertManagers: droppedAlertmanagers,
|
||||
},
|
||||
},
|
||||
},
|
||||
alertmanagerConfig: {
|
||||
result: {
|
||||
alertmanagers: alertmanagerConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('useExternalAmSelector', () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
it('should have one in pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(createMockStoreState([], [], ['some/url/to/am']));
|
||||
});
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
status: 'pending',
|
||||
actualUrl: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState([{ url: 'some/url/to/am/api/v2/alerts' }], [], ['some/url/to/am', 'some/url/to/am1'])
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have two active', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }, { url: 'some/url/to/am1/api/v2/alerts' }],
|
||||
[],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: 'some/url/to/am1/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one dropped, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }],
|
||||
[{ url: 'some/dropped/url/api/v2/alerts' }],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
url: 'some/dropped/url',
|
||||
actualUrl: 'some/dropped/url/api/v2/alerts',
|
||||
status: 'dropped',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StoreState } from '../../../../types';
|
||||
|
||||
const SUFFIX_REGEX = /\/api\/v[1|2]\/alerts/i;
|
||||
type AlertmanagerConfig = { url: string; status: string; actualUrl: string };
|
||||
|
||||
export function useExternalAmSelector(): AlertmanagerConfig[] | undefined {
|
||||
const discoveredAlertmanagers = useSelector(
|
||||
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result?.data
|
||||
);
|
||||
const alertmanagerConfig = useSelector(
|
||||
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result?.alertmanagers
|
||||
);
|
||||
|
||||
if (!discoveredAlertmanagers || !alertmanagerConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const enabledAlertmanagers: AlertmanagerConfig[] = [];
|
||||
const droppedAlertmanagers: AlertmanagerConfig[] = discoveredAlertmanagers?.droppedAlertManagers.map((am) => ({
|
||||
url: am.url.replace(SUFFIX_REGEX, ''),
|
||||
status: 'dropped',
|
||||
actualUrl: am.url,
|
||||
}));
|
||||
|
||||
for (const url of alertmanagerConfig) {
|
||||
if (discoveredAlertmanagers.activeAlertManagers.length === 0) {
|
||||
enabledAlertmanagers.push({
|
||||
url: url,
|
||||
status: 'pending',
|
||||
actualUrl: '',
|
||||
});
|
||||
} else {
|
||||
let found = false;
|
||||
for (const activeAM of discoveredAlertmanagers.activeAlertManagers) {
|
||||
if (activeAM.url === `${url}/api/v2/alerts`) {
|
||||
found = true;
|
||||
enabledAlertmanagers.push({
|
||||
url: activeAM.url.replace(SUFFIX_REGEX, ''),
|
||||
status: 'active',
|
||||
actualUrl: activeAM.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
enabledAlertmanagers.push({
|
||||
url: url,
|
||||
status: 'pending',
|
||||
actualUrl: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...enabledAlertmanagers, ...droppedAlertmanagers];
|
||||
}
|
@ -4,6 +4,7 @@ import {
|
||||
AlertmanagerAlert,
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerGroup,
|
||||
ExternalAlertmanagersResponse,
|
||||
Receiver,
|
||||
Silence,
|
||||
SilenceCreatePayload,
|
||||
@ -29,6 +30,9 @@ import {
|
||||
fetchStatus,
|
||||
deleteAlertManagerConfig,
|
||||
testReceivers,
|
||||
addAlertManagers,
|
||||
fetchExternalAlertmanagers,
|
||||
fetchExternalAlertmanagerConfig,
|
||||
} from '../api/alertmanager';
|
||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||
import {
|
||||
@ -107,6 +111,20 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchExternalAlertmanagersAction = createAsyncThunk(
|
||||
'unifiedAlerting/fetchExternalAlertmanagers',
|
||||
(): Promise<ExternalAlertmanagersResponse> => {
|
||||
return withSerializedError(fetchExternalAlertmanagers());
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchExternalAlertmanagersConfigAction = createAsyncThunk(
|
||||
'unifiedAlerting/fetchExternAlertmanagersConfig',
|
||||
(): Promise<{ alertmanagers: string[] }> => {
|
||||
return withSerializedError(fetchExternalAlertmanagerConfig());
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchRulerRulesAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchRulerRules',
|
||||
({
|
||||
@ -732,3 +750,21 @@ export const updateLotexNamespaceAndGroupAction = createAsyncThunk(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const addExternalAlertmanagersAction = createAsyncThunk(
|
||||
'unifiedAlerting/addExternalAlertmanagers',
|
||||
async (alertManagerUrls: string[], thunkAPI): Promise<void> => {
|
||||
return withAppEvents(
|
||||
withSerializedError(
|
||||
(async () => {
|
||||
await addAlertManagers(alertManagerUrls);
|
||||
thunkAPI.dispatch(fetchExternalAlertmanagersConfigAction());
|
||||
})()
|
||||
),
|
||||
{
|
||||
errorMessage: 'Failed adding alertmanagers',
|
||||
successMessage: 'Alertmanagers updated',
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
deleteAlertManagerConfigAction,
|
||||
testReceiversAction,
|
||||
updateLotexNamespaceAndGroupAction,
|
||||
fetchExternalAlertmanagersAction,
|
||||
fetchExternalAlertmanagersConfigAction,
|
||||
} from './actions';
|
||||
|
||||
export const reducer = combineReducers({
|
||||
@ -54,6 +56,10 @@ export const reducer = combineReducers({
|
||||
testReceivers: createAsyncSlice('testReceivers', testReceiversAction).reducer,
|
||||
updateLotexNamespaceAndGroup: createAsyncSlice('updateLotexNamespaceAndGroup', updateLotexNamespaceAndGroupAction)
|
||||
.reducer,
|
||||
externalAlertmanagers: combineReducers({
|
||||
alertmanagerConfig: createAsyncSlice('alertmanagerConfig', fetchExternalAlertmanagersConfigAction).reducer,
|
||||
discoveredAlertmanagers: createAsyncSlice('discoveredAlertmanagers', fetchExternalAlertmanagersAction).reducer,
|
||||
}),
|
||||
});
|
||||
|
||||
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
||||
|
@ -257,6 +257,19 @@ export interface TestReceiversResult {
|
||||
receivers: TestReceiversResultReceiver[];
|
||||
}
|
||||
|
||||
export interface ExternalAlertmanagers {
|
||||
activeAlertManagers: AlertmanagerUrl[];
|
||||
droppedAlertManagers: AlertmanagerUrl[];
|
||||
}
|
||||
|
||||
export interface AlertmanagerUrl {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ExternalAlertmanagersResponse {
|
||||
data: ExternalAlertmanagers;
|
||||
status: 'string';
|
||||
}
|
||||
export enum AlertManagerImplementation {
|
||||
cortex = 'cortex',
|
||||
prometheus = 'prometheus',
|
||||
|
Loading…
Reference in New Issue
Block a user