Alerting: Refactor useExternalDataSourceAlertmanagers (#81081)

This commit is contained in:
Gilles De Mey 2024-02-01 14:18:43 +01:00 committed by GitHub
parent eb889c41ee
commit 7f6806e220
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 226 additions and 223 deletions

View File

@ -27,6 +27,12 @@ export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (
export const alertingApi = createApi({
reducerPath: 'alertingApi',
baseQuery: backendSrvBaseQuery(),
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration', 'OnCallIntegrations', 'OrgMigrationState'],
tagTypes: [
'AlertmanagerChoice',
'AlertmanagerConfiguration',
'OnCallIntegrations',
'OrgMigrationState',
'DataSourceSettings',
],
endpoints: () => ({}),
});

View File

@ -0,0 +1,15 @@
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { alertingApi } from './alertingApi';
export const dataSourcesApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
getAllDataSourceSettings: build.query<Array<DataSourceSettings<DataSourceJsonData>>, void>({
query: () => ({ url: 'api/datasources' }),
// we'll create individual cache entries for each datasource UID
providesTags: (result) => {
return result ? result.map(({ uid }) => ({ type: 'DataSourceSettings', id: uid })) : ['DataSourceSettings'];
},
}),
}),
});

View File

@ -5,11 +5,11 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Badge, CallToActionCard, Card, Icon, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
import { ExternalDataSourceAM } from '../../hooks/useExternalAmSelector';
import { ExternalAlertmanagerDataSourceWithStatus } from '../../hooks/useExternalAmSelector';
import { makeDataSourceLink } from '../../utils/misc';
export interface ExternalAlertManagerDataSourcesProps {
alertmanagers: ExternalDataSourceAM[];
alertmanagers: ExternalAlertmanagerDataSourceWithStatus[];
inactive: boolean;
}
@ -39,7 +39,7 @@ export function ExternalAlertmanagerDataSources({ alertmanagers, inactive }: Ext
{alertmanagers.length > 0 && (
<div className={styles.externalDs}>
{alertmanagers.map((am) => (
<ExternalAMdataSourceCard key={am.dataSource.uid} alertmanager={am} inactive={inactive} />
<ExternalAMdataSourceCard key={am.dataSourceSettings.uid} alertmanager={am} inactive={inactive} />
))}
</div>
)}
@ -48,20 +48,20 @@ export function ExternalAlertmanagerDataSources({ alertmanagers, inactive }: Ext
}
interface ExternalAMdataSourceCardProps {
alertmanager: ExternalDataSourceAM;
alertmanager: ExternalAlertmanagerDataSourceWithStatus;
inactive: boolean;
}
export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMdataSourceCardProps) {
const styles = useStyles2(getStyles);
const { dataSource, status, statusInconclusive, url } = alertmanager;
const { dataSourceSettings, status } = alertmanager;
return (
<Card>
<Card.Heading className={styles.externalHeading}>
{dataSource.name}{' '}
{statusInconclusive && (
{dataSourceSettings.name}{' '}
{status === 'inconclusive' && (
<Tooltip content="Multiple Alertmanagers have the same URL configured. The state might be inconclusive.">
<Icon name="exclamation-triangle" size="md" className={styles.externalWarningIcon} />
</Tooltip>
@ -90,9 +90,9 @@ export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMd
/>
)}
</Card.Tags>
<Card.Meta>{url}</Card.Meta>
<Card.Meta>{dataSourceSettings.url}</Card.Meta>
<Card.Actions>
<LinkButton href={makeDataSourceLink(dataSource)} size="sm" variant="secondary">
<LinkButton href={makeDataSourceLink(dataSourceSettings.uid)} size="sm" variant="secondary">
Go to datasource
</LinkButton>
</Card.Actions>

View File

@ -23,6 +23,9 @@ export const ExternalAlertmanagers = () => {
const dispatch = useDispatch();
const externalDsAlertManagers = useExternalDataSourceAlertmanagers();
const gmaHandlingAlertmanagers = externalDsAlertManagers.filter(
(settings) => settings.dataSourceSettings.jsonData.handleGrafanaManagedAlerts === true
);
const {
useSaveExternalAlertmanagersConfigMutation,
@ -71,7 +74,7 @@ export const ExternalAlertmanagers = () => {
</div>
<ExternalAlertmanagerDataSources
alertmanagers={externalDsAlertManagers}
alertmanagers={gmaHandlingAlertmanagers}
inactive={alertmanagersChoice === AlertmanagerChoice.Internal}
/>
</div>

View File

@ -53,7 +53,7 @@ export function RuleListErrors(): ReactElement {
result.push(
<>
Failed to load the data source configuration for{' '}
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
{dataSource.name}
</a>
: {error.message || 'Unknown error.'}
@ -65,7 +65,7 @@ export function RuleListErrors(): ReactElement {
result.push(
<>
Failed to load rules state from{' '}
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
{dataSource.name}
</a>
: {error.message || 'Unknown error.'}
@ -77,7 +77,7 @@ export function RuleListErrors(): ReactElement {
result.push(
<>
Failed to load rules config from{' '}
<a href={makeDataSourceLink(dataSource)} className={styles.dsLink}>
<a href={makeDataSourceLink(dataSource.uid)} className={styles.dsLink}>
{dataSource.name}
</a>
: {error.message || 'Unknown error.'}

View File

@ -1,16 +1,15 @@
import { renderHook, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import React from 'react';
import { Provider } from 'react-redux';
import { rest } from 'msw';
import { SetupServer, setupServer } from 'msw/node';
import { TestProvider } from 'test/helpers/TestProvider';
import 'whatwg-fetch';
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { config, setBackendSrv } from '@grafana/runtime';
import { DataSourceSettings } from '@grafana/data';
import { setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { mockDataSource, mockDataSourcesStore, mockStore } from '../mocks';
import { mockAlertmanagersResponse } from '../mocks/alertmanagerApi';
import { useExternalDataSourceAlertmanagers } from './useExternalAmSelector';
@ -31,46 +30,42 @@ afterAll(() => {
});
describe('useExternalDataSourceAlertmanagers', () => {
it('Should merge data sources information from config and api responses', async () => {
it('Should get the correct data source settings', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockDataSourcesStore({
dataSources: [dsSettings],
});
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } });
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].dataSource.uid).toBe('1');
expect(current[0].url).toBe('http://grafana.com');
expect(current[0].dataSourceSettings.uid).toBe('1');
expect(current[0].dataSourceSettings.url).toBe('http://grafana.com');
});
});
it('Should have uninterested state if data source does not want alerts', async () => {
// Arrange
setupAlertmanagerDataSource(server, { url: 'http://grafana.com', jsonData: { handleGrafanaManagedAlerts: false } });
mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } });
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('uninterested');
});
});
it('Should have active state if available in the activeAlertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
});
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
@ -78,32 +73,20 @@ describe('useExternalDataSourceAlertmanagers', () => {
},
});
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('active');
expect(current[0].statusInconclusive).toBe(false);
});
});
it('Should have dropped state if available in the droppedAlertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
});
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [],
@ -111,10 +94,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
},
});
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
@ -122,22 +103,12 @@ describe('useExternalDataSourceAlertmanagers', () => {
expect(current).toHaveLength(1);
expect(current[0].status).toBe('dropped');
expect(current[0].statusInconclusive).toBe(false);
});
});
it('Should have pending state if not available neither in dropped nor in active alertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource();
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
});
setupAlertmanagerDataSource(server);
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [],
@ -145,10 +116,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
},
});
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
@ -156,22 +125,12 @@ describe('useExternalDataSourceAlertmanagers', () => {
expect(current).toHaveLength(1);
expect(current[0].status).toBe('pending');
expect(current[0].statusInconclusive).toBe(false);
});
});
it('Should match Alertmanager url when datasource url does not have protocol specified', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'localhost:9093' });
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
});
setupAlertmanagerDataSource(server, { url: 'localhost:9093' });
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://localhost:9093/api/v2/alerts' }],
@ -179,10 +138,8 @@ describe('useExternalDataSourceAlertmanagers', () => {
},
});
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper: TestProvider });
await waitFor(() => {
// Assert
@ -190,11 +147,34 @@ describe('useExternalDataSourceAlertmanagers', () => {
expect(current).toHaveLength(1);
expect(current[0].status).toBe('active');
expect(current[0].url).toBe('localhost:9093');
expect(current[0].dataSourceSettings.url).toBe('localhost:9093');
});
});
it('Should have inconclusive state when there are many Alertmanagers of the same URL', async () => {
it('Should have inconclusive state when there are many Alertmanagers of the same URL on both active and inactive', async () => {
// Arrange
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
droppedAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
},
});
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), {
wrapper: TestProvider,
});
await waitFor(() => {
// Assert
expect(result.current).toHaveLength(1);
expect(result.current[0].status).toBe('inconclusive');
});
});
it('Should have not have inconclusive state when all Alertmanagers of the same URL are active', async () => {
// Arrange
mockAlertmanagersResponse(server, {
data: {
@ -203,72 +183,40 @@ describe('useExternalDataSourceAlertmanagers', () => {
},
});
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
config.datasources = {
'External Alertmanager': dsInstanceSettings,
};
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
});
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
setupAlertmanagerDataSource(server, { url: 'http://grafana.com' });
// Act
const { result } = renderHook(() => useExternalDataSourceAlertmanagers(), {
wrapper,
wrapper: TestProvider,
});
await waitFor(() => {
// Assert
expect(result.current).toHaveLength(1);
expect(result.current[0].status).toBe('active');
expect(result.current[0].statusInconclusive).toBe(true);
});
});
});
function setupAlertmanagerDataSource(partialDsSettings?: Partial<DataSourceSettings<AlertManagerDataSourceJsonData>>) {
function setupAlertmanagerDataSource(
server: SetupServer,
partialDsSettings?: Partial<DataSourceSettings<AlertManagerDataSourceJsonData>>
) {
const dsCommonConfig = {
uid: '1',
name: 'External Alertmanager',
type: 'alertmanager',
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
jsonData: { handleGrafanaManagedAlerts: true },
};
const dsInstanceSettings = mockDataSource(dsCommonConfig);
const dsSettings = mockApiDataSource({
const dsSettings = {
...dsCommonConfig,
...partialDsSettings,
});
return { dsSettings, dsInstanceSettings };
}
function mockApiDataSource(partial: Partial<DataSourceSettings<DataSourceJsonData, {}>> = {}) {
const dsSettings: DataSourceSettings<DataSourceJsonData, {}> = {
uid: '1',
id: 1,
name: '',
url: '',
type: '',
access: '',
orgId: 1,
typeLogoUrl: '',
typeName: '',
user: '',
database: '',
basicAuth: false,
isDefault: false,
basicAuthUser: '',
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
secureJsonFields: {},
readOnly: false,
withCredentials: false,
...partial,
};
return dsSettings;
server.use(
rest.get('/api/datasources', (_req, res, ctx) => {
return res(ctx.json([dsSettings]));
})
);
}

View File

@ -1,76 +1,114 @@
import { countBy, keyBy } from 'lodash';
import { createSelector } from 'reselect';
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { StoreState, useSelector } from 'app/types';
import { DataSourceSettings } from '@grafana/data';
import { AlertManagerDataSourceJsonData, ExternalAlertmanagers } from 'app/plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { getAlertManagerDataSources } from '../utils/datasource';
import { dataSourcesApi } from '../api/dataSourcesApi';
import { isAlertmanagerDataSource } from '../utils/datasource';
export interface ExternalDataSourceAM {
dataSource: DataSourceInstanceSettings<AlertManagerDataSourceJsonData>;
url?: string;
status: 'active' | 'pending' | 'dropped';
statusInconclusive?: boolean;
type ConnectionStatus = 'active' | 'pending' | 'dropped' | 'inconclusive' | 'uninterested' | 'unknown';
export interface ExternalAlertmanagerDataSourceWithStatus {
dataSourceSettings: DataSourceSettings<AlertManagerDataSourceJsonData>;
status: ConnectionStatus;
}
export function useExternalDataSourceAlertmanagers(): ExternalDataSourceAM[] {
const { useGetExternalAlertmanagersQuery } = alertmanagerApi;
const { currentData: discoveredAlertmanagers } = useGetExternalAlertmanagersQuery();
/**
* Returns all configured Alertmanager data sources and their connection status with the internal ruler
*/
export function useExternalDataSourceAlertmanagers(): ExternalAlertmanagerDataSourceWithStatus[] {
// firstly we'll fetch the settings for all datasources and filter for "alertmanager" type
const { alertmanagerDataSources } = dataSourcesApi.endpoints.getAllDataSourceSettings.useQuery(undefined, {
refetchOnReconnect: true,
selectFromResult: (result) => {
const alertmanagerDataSources = result.currentData?.filter(isAlertmanagerDataSource) ?? [];
return { ...result, alertmanagerDataSources };
},
});
const externalDsAlertManagers = getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
const alertmanagerDatasources = useSelector(
createSelector(
(state: StoreState) => state.dataSources.dataSources.filter((ds) => ds.type === 'alertmanager'),
(datasources) => keyBy(datasources, (ds) => ds.uid)
)
// we'll also fetch the configuration for which Alertmanagers we are forwarding Grafana-managed alerts too
// @TODO use polling when we have one or more alertmanagers in pending state
const { currentData: externalAlertmanagers } = alertmanagerApi.endpoints.getExternalAlertmanagers.useQuery(
undefined,
{ refetchOnReconnect: true }
);
const droppedAMUrls = countBy(discoveredAlertmanagers?.droppedAlertManagers, (x) => x.url);
const activeAMUrls = countBy(discoveredAlertmanagers?.activeAlertManagers, (x) => x.url);
if (!alertmanagerDataSources) {
return [];
}
return externalDsAlertManagers.map<ExternalDataSourceAM>((dsAm) => {
const dsSettings = alertmanagerDatasources[dsAm.uid];
if (!dsSettings) {
return {
dataSource: dsAm,
status: 'pending',
};
}
const amUrl = getDataSourceUrlWithProtocol(dsSettings);
const amStatusUrl = `${amUrl}/api/v2/alerts`;
const matchingDroppedUrls = droppedAMUrls[amStatusUrl] ?? 0;
const matchingActiveUrls = activeAMUrls[amStatusUrl] ?? 0;
const isDropped = matchingDroppedUrls > 0;
const isActive = matchingActiveUrls > 0;
// Multiple Alertmanagers of the same URL may exist (e.g. with different credentials)
// Alertmanager response only contains URLs, so in case of duplication, we are not able
// to distinguish which is which, resulting in an inconclusive status.
const isStatusInconclusive = matchingDroppedUrls + matchingActiveUrls > 1;
const status = isDropped ? 'dropped' : isActive ? 'active' : 'pending';
return alertmanagerDataSources.map<ExternalAlertmanagerDataSourceWithStatus>((dataSourceSettings) => {
const status = externalAlertmanagers
? determineAlertmanagerConnectionStatus(externalAlertmanagers, dataSourceSettings)
: 'unknown';
return {
dataSource: dsAm,
url: dsSettings.url,
dataSourceSettings,
status,
statusInconclusive: isStatusInconclusive,
};
});
}
function getDataSourceUrlWithProtocol<T extends DataSourceJsonData>(dsSettings: DataSourceSettings<T>) {
const hasProtocol = new RegExp('^[^:]*://').test(dsSettings.url);
if (!hasProtocol) {
return `http://${dsSettings.url}`; // Grafana append http protocol if there is no any
// using the information from /api/v1/ngalert/alertmanagers we should derive the connection status of a single data source
function determineAlertmanagerConnectionStatus(
externalAlertmanagers: ExternalAlertmanagers,
dataSourceSettings: DataSourceSettings<AlertManagerDataSourceJsonData>
): ConnectionStatus {
const isInterestedInAlerts = dataSourceSettings.jsonData.handleGrafanaManagedAlerts;
if (!isInterestedInAlerts) {
return 'uninterested';
}
return dsSettings.url;
const isActive =
externalAlertmanagers?.activeAlertManagers.some((am) => {
return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url);
}) ?? [];
const isDropped =
externalAlertmanagers?.droppedAlertManagers.some((am) => {
return isAlertmanagerMatchByURL(dataSourceSettings.url, am.url);
}) ?? [];
// the Alertmanager is being adopted (pending) if it is interested in handling alerts but not in either "active" or "dropped"
const isPending = !isActive && !isDropped;
if (isPending) {
return 'pending';
}
// Multiple Alertmanagers of the same URL may exist (e.g. with different credentials)
// Alertmanager response only contains URLs, so when the URL exists in both active and dropped, we are not able
// to distinguish which is which, resulting in an inconclusive status.
const isInconclusive = isActive && isDropped;
if (isInconclusive) {
return 'inconclusive';
}
// if we get here, it's neither "uninterested", nor "inconclusive" nor "pending"
if (isActive) {
return 'active';
} else if (isDropped) {
return 'dropped';
}
return 'unknown';
}
// the vanilla Alertmanager and Mimir Alertmanager mount their API endpoints on different sub-paths
// Cortex also uses the same paths as Mimir
const MIMIR_ALERTMANAGER_PATH = '/alertmanager/api/v2/alerts';
const VANILLA_ALERTMANAGER_PATH = '/api/v2/alerts';
// when using the Mimir Alertmanager, those paths are mounted under "/alertmanager"
function isAlertmanagerMatchByURL(dataSourceUrl: string, alertmanagerUrl: string) {
const normalizedUrl = normalizeDataSourceURL(dataSourceUrl);
const prometheusAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${VANILLA_ALERTMANAGER_PATH}`;
const mimirAlertmanagerMatch = alertmanagerUrl === `${normalizedUrl}${MIMIR_ALERTMANAGER_PATH}`;
return prometheusAlertmanagerMatch || mimirAlertmanagerMatch;
}
// Grafana prepends the http protocol if there isn't one, but it doesn't store that in the datasource settings
function normalizeDataSourceURL(url: string) {
const hasProtocol = new RegExp('^[^:]*://').test(url);
return hasProtocol ? url : `http://${url}`;
}

View File

@ -602,19 +602,6 @@ export const grantUserPermissions = (permissions: AccessControlAction[]) => {
.mockImplementation((action) => permissions.includes(action as AccessControlAction));
};
export function mockDataSourcesStore(partial?: Partial<StoreState['dataSources']>) {
const defaultState = configureStore().getState();
const store = configureStore({
...defaultState,
dataSources: {
...defaultState.dataSources,
...partial,
},
});
return store;
}
export function mockUnifiedAlertingStore(unifiedAlerting?: Partial<StoreState['unifiedAlerting']>) {
const defaultState = configureStore().getState();

View File

@ -1,12 +1,9 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { config } from '@grafana/runtime';
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { isValidPrometheusDuration, parsePrometheusDuration } from './time';
export function getAllDataSources(): Array<
DataSourceInstanceSettings<DataSourceJsonData | AlertManagerDataSourceJsonData>
> {
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
return Object.values(config.datasources);
}

View File

@ -1,4 +1,4 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import {
@ -51,12 +51,22 @@ export function getRulesDataSource(rulesSourceName: string) {
export function getAlertManagerDataSources() {
return getAllDataSources()
.filter(
(ds): ds is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> => ds.type === DataSourceType.Alertmanager
)
.filter(isAlertmanagerDataSourceInstance)
.sort((a, b) => a.name.localeCompare(b.name));
}
export function isAlertmanagerDataSourceInstance(
dataSource: DataSourceInstanceSettings
): dataSource is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> {
return dataSource.type === DataSourceType.Alertmanager;
}
export function isAlertmanagerDataSource(
dataSource: DataSourceSettings
): dataSource is DataSourceSettings<AlertManagerDataSourceJsonData> {
return dataSource.type === DataSourceType.Alertmanager;
}
export function getExternalDsAlertManagers() {
return getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
}
@ -205,10 +215,9 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings<Da
}
export function getAlertmanagerDataSourceByName(name: string) {
return getAllDataSources().find(
(source): source is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> =>
source.name === name && source.type === 'alertmanager'
);
return getAllDataSources()
.filter(isAlertmanagerDataSourceInstance)
.find((source) => source.name === name);
}
export function getRulesSourceByName(name: string): RulesSource | undefined {

View File

@ -1,6 +1,6 @@
import { sortBy } from 'lodash';
import { UrlQueryMap, Labels, DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { UrlQueryMap, Labels } from '@grafana/data';
import { GrafanaEdition } from '@grafana/data/src/types/config';
import { config } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema';
@ -137,8 +137,8 @@ export function makeLabelBasedSilenceLink(alertManagerSourceName: string, labels
return createUrl('/alerting/silence/new', silenceUrlParams);
}
export function makeDataSourceLink<T extends DataSourceJsonData>(dataSource: DataSourceInstanceSettings<T>) {
return createUrl(`/datasources/edit/${dataSource.uid}`);
export function makeDataSourceLink(uid: string) {
return createUrl(`/datasources/edit/${uid}`);
}
export function makeFolderLink(folderUID: string): string {