mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Filter rules list (#32818)
This commit is contained in:
parent
ffe90b7abf
commit
345d9f93fe
@ -24,6 +24,7 @@ export interface DataSourcePickerProps {
|
|||||||
mixed?: boolean;
|
mixed?: boolean;
|
||||||
dashboard?: boolean;
|
dashboard?: boolean;
|
||||||
metrics?: boolean;
|
metrics?: boolean;
|
||||||
|
type?: string | string[];
|
||||||
annotations?: boolean;
|
annotations?: boolean;
|
||||||
variables?: boolean;
|
variables?: boolean;
|
||||||
alerting?: boolean;
|
alerting?: boolean;
|
||||||
@ -108,9 +109,10 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDataSourceOptions() {
|
getDataSourceOptions() {
|
||||||
const { tracing, metrics, mixed, dashboard, variables, annotations, pluginId, alerting, filter } = this.props;
|
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter } = this.props;
|
||||||
const options = this.dataSourceSrv
|
const options = this.dataSourceSrv
|
||||||
.getList({
|
.getList({
|
||||||
|
alerting,
|
||||||
tracing,
|
tracing,
|
||||||
metrics,
|
metrics,
|
||||||
dashboard,
|
dashboard,
|
||||||
@ -118,8 +120,8 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
|||||||
variables,
|
variables,
|
||||||
annotations,
|
annotations,
|
||||||
pluginId,
|
pluginId,
|
||||||
alerting,
|
|
||||||
filter,
|
filter,
|
||||||
|
type,
|
||||||
})
|
})
|
||||||
.map((ds) => ({
|
.map((ds) => ({
|
||||||
value: ds.name,
|
value: ds.name,
|
||||||
|
@ -60,6 +60,9 @@ export interface GetDataSourceListFilters {
|
|||||||
|
|
||||||
/** apply a function to filter */
|
/** apply a function to filter */
|
||||||
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
|
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
|
||||||
|
|
||||||
|
/** Only returns datasources matching the specified types (ie. Loki, Prometheus) */
|
||||||
|
type?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let singletonInstance: DataSourceSrv;
|
let singletonInstance: DataSourceSrv;
|
||||||
|
@ -14,11 +14,13 @@ import {
|
|||||||
mockPromRecordingRule,
|
mockPromRecordingRule,
|
||||||
mockPromRuleGroup,
|
mockPromRuleGroup,
|
||||||
mockPromRuleNamespace,
|
mockPromRuleNamespace,
|
||||||
|
MockDataSourceSrv,
|
||||||
} from './mocks';
|
} from './mocks';
|
||||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
import { SerializedError } from '@reduxjs/toolkit';
|
import { SerializedError } from '@reduxjs/toolkit';
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
jest.mock('./api/prometheus');
|
jest.mock('./api/prometheus');
|
||||||
jest.mock('./utils/config');
|
jest.mock('./utils/config');
|
||||||
@ -66,11 +68,16 @@ const ui = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('RuleList', () => {
|
describe('RuleList', () => {
|
||||||
afterEach(() => jest.resetAllMocks());
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
setDataSourceSrv(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
it('load & show rule groups from multiple cloud data sources', async () => {
|
it('load & show rule groups from multiple cloud data sources', async () => {
|
||||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||||
|
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
|
|
||||||
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
|
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
|
||||||
if (dataSourceName === dataSources.prom.name) {
|
if (dataSourceName === dataSources.prom.name) {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
@ -145,6 +152,7 @@ describe('RuleList', () => {
|
|||||||
|
|
||||||
it('expand rule group, rule and alert details', async () => {
|
it('expand rule group, rule and alert details', async () => {
|
||||||
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
|
||||||
|
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
|
||||||
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
|
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
|
||||||
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
|
@ -7,6 +7,7 @@ import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
|||||||
import { NoRulesSplash } from './components/rules/NoRulesCTA';
|
import { NoRulesSplash } from './components/rules/NoRulesCTA';
|
||||||
import { SystemOrApplicationRules } from './components/rules/SystemOrApplicationRules';
|
import { SystemOrApplicationRules } from './components/rules/SystemOrApplicationRules';
|
||||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||||
|
import { useFilteredRules } from './hooks/useFilteredRules';
|
||||||
import { fetchAllPromAndRulerRulesAction } from './state/actions';
|
import { fetchAllPromAndRulerRulesAction } from './state/actions';
|
||||||
import {
|
import {
|
||||||
getAllRulesSourceNames,
|
getAllRulesSourceNames,
|
||||||
@ -19,6 +20,7 @@ import { ThresholdRules } from './components/rules/ThresholdRules';
|
|||||||
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
||||||
import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
|
import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
|
||||||
import { isRulerNotSupportedResponse } from './utils/rules';
|
import { isRulerNotSupportedResponse } from './utils/rules';
|
||||||
|
import RulesFilter from './components/rules/RulesFilter';
|
||||||
|
|
||||||
export const RuleList: FC = () => {
|
export const RuleList: FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -72,8 +74,9 @@ export const RuleList: FC = () => {
|
|||||||
const showNewAlertSplash = dispatched && !loading && !haveResults;
|
const showNewAlertSplash = dispatched && !loading && !haveResults;
|
||||||
|
|
||||||
const combinedNamespaces = useCombinedRuleNamespaces();
|
const combinedNamespaces = useCombinedRuleNamespaces();
|
||||||
|
const filteredNamespaces = useFilteredRules(combinedNamespaces);
|
||||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||||
const sorted = combinedNamespaces
|
const sorted = filteredNamespaces
|
||||||
.map((namespace) => ({
|
.map((namespace) => ({
|
||||||
...namespace,
|
...namespace,
|
||||||
groups: namespace.groups.sort((a, b) => a.name.localeCompare(b.name)),
|
groups: namespace.groups.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
@ -83,7 +86,7 @@ export const RuleList: FC = () => {
|
|||||||
sorted.filter((ns) => ns.rulesSource === GRAFANA_RULES_SOURCE_NAME),
|
sorted.filter((ns) => ns.rulesSource === GRAFANA_RULES_SOURCE_NAME),
|
||||||
sorted.filter((ns) => isCloudRulesSource(ns.rulesSource)),
|
sorted.filter((ns) => isCloudRulesSource(ns.rulesSource)),
|
||||||
];
|
];
|
||||||
}, [combinedNamespaces]);
|
}, [filteredNamespaces]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
|
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
|
||||||
@ -119,12 +122,16 @@ export const RuleList: FC = () => {
|
|||||||
</InfoBox>
|
</InfoBox>
|
||||||
)}
|
)}
|
||||||
{!showNewAlertSplash && (
|
{!showNewAlertSplash && (
|
||||||
<div className={styles.buttonsContainer}>
|
<>
|
||||||
<div />
|
<RulesFilter />
|
||||||
<a href="/alerting/new">
|
<div className={styles.break} />
|
||||||
<Button icon="plus">New alert rule</Button>
|
<div className={styles.buttonsContainer}>
|
||||||
</a>
|
<div />
|
||||||
</div>
|
<a href="/alerting/new">
|
||||||
|
<Button icon="plus">New alert rule</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showNewAlertSplash && <NoRulesSplash />}
|
{showNewAlertSplash && <NoRulesSplash />}
|
||||||
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
||||||
@ -134,6 +141,12 @@ export const RuleList: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
break: css`
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
margin-bottom: ${theme.spacing.md};
|
||||||
|
border-bottom: solid 1px ${theme.colors.border2};
|
||||||
|
`,
|
||||||
iconError: css`
|
iconError: css`
|
||||||
color: ${theme.palette.red};
|
color: ${theme.palette.red};
|
||||||
margin-right: ${theme.spacing.md};
|
margin-right: ${theme.spacing.md};
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
import React, { FormEvent, useState } from 'react';
|
||||||
|
import { Button, Icon, Input, Label, RadioButtonGroup, useStyles } from '@grafana/ui';
|
||||||
|
import { DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
import { getFiltersFromUrlParams } from '../../utils/misc';
|
||||||
|
import { DataSourcePicker } from '@grafana/runtime';
|
||||||
|
|
||||||
|
const RulesFilter = () => {
|
||||||
|
const [queryParams, setQueryParams] = useQueryParams();
|
||||||
|
// This key is used to force a rerender on the inputs when the filters are cleared
|
||||||
|
const [filterKey, setFilterKey] = useState<number>(Math.floor(Math.random() * 100));
|
||||||
|
const dataSourceKey = `dataSource-${filterKey}`;
|
||||||
|
const queryStringKey = `queryString-${filterKey}`;
|
||||||
|
|
||||||
|
const { dataSource, alertState, queryString } = getFiltersFromUrlParams(queryParams);
|
||||||
|
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
const stateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ label: key, value }));
|
||||||
|
|
||||||
|
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => {
|
||||||
|
setQueryParams({ dataSource: dataSourceValue.name });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueryStringChange = debounce((e: FormEvent<HTMLInputElement>) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setQueryParams({ queryString: target.value || null });
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
const handleAlertStateChange = (value: string) => {
|
||||||
|
setQueryParams({ alertState: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearFiltersClick = () => {
|
||||||
|
setQueryParams({
|
||||||
|
alertState: null,
|
||||||
|
queryString: null,
|
||||||
|
dataSource: null,
|
||||||
|
});
|
||||||
|
setFilterKey(filterKey + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchIcon = <Icon name={'search'} />;
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.inputWidth}>
|
||||||
|
<Label>Select data source</Label>
|
||||||
|
<DataSourcePicker
|
||||||
|
key={dataSourceKey}
|
||||||
|
alerting
|
||||||
|
noDefault
|
||||||
|
current={dataSource}
|
||||||
|
onChange={handleDataSourceChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
||||||
|
<div className={styles.flexRow}>
|
||||||
|
<div className={styles.rowChild}>
|
||||||
|
<Label>Search by name or label</Label>
|
||||||
|
<Input
|
||||||
|
key={queryStringKey}
|
||||||
|
className={styles.inputWidth}
|
||||||
|
prefix={searchIcon}
|
||||||
|
onChange={handleQueryStringChange}
|
||||||
|
defaultValue={queryString}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rowChild}>
|
||||||
|
<RadioButtonGroup options={stateOptions} value={alertState} onChange={handleAlertStateChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(dataSource || alertState || queryString) && (
|
||||||
|
<div className={styles.flexRow}>
|
||||||
|
<Button fullWidth={false} icon="times" variant="secondary" onClick={handleClearFiltersClick}>
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid ${theme.colors.border1};
|
||||||
|
padding-bottom: ${theme.spacing.sm};
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
margin-bottom: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
inputWidth: css`
|
||||||
|
width: 340px;
|
||||||
|
flex-grow: 0;
|
||||||
|
`,
|
||||||
|
flexRow: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
`,
|
||||||
|
spaceBetween: css`
|
||||||
|
justify-content: space-between;
|
||||||
|
`,
|
||||||
|
rowChild: css`
|
||||||
|
& + & {
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
clearButton: css`
|
||||||
|
align-self: flex-end;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RulesFilter;
|
@ -36,7 +36,7 @@ export const SystemOrApplicationRules: FC<Props> = ({ namespaces }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{namespaces.map(({ rulesSource, name, groups }) =>
|
{namespaces?.map(({ rulesSource, name, groups }) =>
|
||||||
groups.map((group) => (
|
groups.map((group) => (
|
||||||
<RulesGroup
|
<RulesGroup
|
||||||
group={group}
|
group={group}
|
||||||
@ -46,7 +46,7 @@ export const SystemOrApplicationRules: FC<Props> = ({ namespaces }) => {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
{namespaces?.length === 0 && !!rulesDataSources.length && <p>No rules found.</p>}
|
{namespaces?.length === 0 && !dataSourcesLoading.length && !!rulesDataSources.length && <p>No rules found.</p>}
|
||||||
{!rulesDataSources.length && <p>There are no Prometheus or Loki datas sources configured.</p>}
|
{!rulesDataSources.length && <p>There are no Prometheus or Loki datas sources configured.</p>}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -87,7 +87,7 @@ export function useCombinedRuleNamespaces(): CombinedRuleNamespace[] {
|
|||||||
ns.groups.push(combinedGroup);
|
ns.groups.push(combinedGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
group.rules.forEach((rule) => {
|
(group.rules ?? []).forEach((rule) => {
|
||||||
const existingRule = combinedGroup!.rules.find((existingRule) => {
|
const existingRule = combinedGroup!.rules.find((existingRule) => {
|
||||||
return !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule);
|
return !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
|
||||||
|
import { isCloudRulesSource } from '../utils/datasource';
|
||||||
|
import { isAlertingRule } from '../utils/rules';
|
||||||
|
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||||
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
|
export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => {
|
||||||
|
const [queryParams] = useQueryParams();
|
||||||
|
const filters = getFiltersFromUrlParams(queryParams);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!filters.queryString && !filters.dataSource && !filters.alertState) {
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
const filteredNamespaces = namespaces
|
||||||
|
// Filter by data source
|
||||||
|
// TODO: filter by multiple data sources for grafana-managed alerts
|
||||||
|
.filter(({ rulesSource }) =>
|
||||||
|
filters.dataSource && isCloudRulesSource(rulesSource) ? rulesSource.name === filters.dataSource : true
|
||||||
|
)
|
||||||
|
// If a namespace and group have rules that match the rules filters then keep them.
|
||||||
|
.reduce(reduceNamespaces(filters), [] as CombinedRuleNamespace[]);
|
||||||
|
return filteredNamespaces;
|
||||||
|
}, [namespaces, filters]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reduceNamespaces = (filters: RuleFilterState) => {
|
||||||
|
return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => {
|
||||||
|
const groups = namespace.groups.reduce(reduceGroups(filters), [] as CombinedRuleGroup[]);
|
||||||
|
|
||||||
|
if (groups.length) {
|
||||||
|
namespaceAcc.push({
|
||||||
|
...namespace,
|
||||||
|
groups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaceAcc;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reduces groups to only groups that have rules matching the filters
|
||||||
|
const reduceGroups = (filters: RuleFilterState) => {
|
||||||
|
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
||||||
|
const rules = group.rules.filter((rule) => {
|
||||||
|
let shouldKeep = true;
|
||||||
|
// Query strings can match alert name, label keys, and label values
|
||||||
|
if (filters.queryString) {
|
||||||
|
const normalizedQueryString = filters.queryString.toLocaleLowerCase();
|
||||||
|
const doesNameContainsQueryString = rule.name?.toLocaleLowerCase().includes(normalizedQueryString);
|
||||||
|
|
||||||
|
const doLabelsContainQueryString = Object.entries(rule.labels || {}).some(
|
||||||
|
([key, value]) =>
|
||||||
|
key.toLocaleLowerCase().includes(normalizedQueryString) ||
|
||||||
|
value.toLocaleLowerCase().includes(normalizedQueryString)
|
||||||
|
);
|
||||||
|
shouldKeep = doesNameContainsQueryString || doLabelsContainQueryString;
|
||||||
|
}
|
||||||
|
if (filters.alertState) {
|
||||||
|
const matchesAlertState = Boolean(
|
||||||
|
rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filters.alertState
|
||||||
|
);
|
||||||
|
|
||||||
|
shouldKeep = shouldKeep && matchesAlertState;
|
||||||
|
}
|
||||||
|
return shouldKeep;
|
||||||
|
});
|
||||||
|
// Add rules to the group that match the rule list filters
|
||||||
|
if (rules.length) {
|
||||||
|
groupAcc.push({
|
||||||
|
...group,
|
||||||
|
rules,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return groupAcc;
|
||||||
|
};
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
|
import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta, ScopedVars } from '@grafana/data';
|
||||||
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||||
import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
|
import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
import DatasourceSrv from 'app/features/plugins/datasource_srv';
|
||||||
|
import { DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
|
||||||
|
|
||||||
let nextDataSourceId = 1;
|
let nextDataSourceId = 1;
|
||||||
|
|
||||||
@ -91,3 +93,47 @@ export const mockPromRuleNamespace = (partial: Partial<RuleNamespace> = {}): Rul
|
|||||||
...partial,
|
...partial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class MockDataSourceSrv implements DataSourceSrv {
|
||||||
|
// @ts-ignore
|
||||||
|
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
|
||||||
|
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
|
||||||
|
private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
|
||||||
|
// @ts-ignore
|
||||||
|
private templateSrv = {
|
||||||
|
getVariables: () => [],
|
||||||
|
replace: (name: any) => name,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(datasources: Record<string, DataSourceInstanceSettings>) {
|
||||||
|
this.settingsMapByName = Object.values(datasources).reduce<Record<string, DataSourceInstanceSettings>>(
|
||||||
|
(acc, ds) => {
|
||||||
|
acc[ds.name] = ds;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
for (const dsSettings of Object.values(this.settingsMapByName)) {
|
||||||
|
this.settingsMapByUid[dsSettings.uid] = dsSettings;
|
||||||
|
this.settingsMapById[dsSettings.id] = dsSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||||
|
return Promise.reject(new Error('not implemented'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of data sources
|
||||||
|
*/
|
||||||
|
getList(filters?: GetDataSourceListFilters): DataSourceInstanceSettings[] {
|
||||||
|
return DatasourceSrv.prototype.getList.call(this, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings and plugin metadata by name or uid
|
||||||
|
*/
|
||||||
|
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined {
|
||||||
|
return DatasourceSrv.prototype.getInstanceSettings.call(this, nameOrUid) || { meta: { info: { logos: {} } } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { urlUtil } from '@grafana/data';
|
import { urlUtil, UrlQueryMap } from '@grafana/data';
|
||||||
|
import { RuleFilterState } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
export function createExploreLink(dataSourceName: string, query: string) {
|
export function createExploreLink(dataSourceName: string, query: string) {
|
||||||
return urlUtil.renderUrl(config.appSubUrl + '/explore', {
|
return urlUtil.renderUrl(config.appSubUrl + '/explore', {
|
||||||
@ -33,3 +34,11 @@ export function arrayToRecord(items: Array<{ key: string; value: string }>): Rec
|
|||||||
return rec;
|
return rec;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): RuleFilterState => {
|
||||||
|
const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
|
||||||
|
const alertState = queryParams['alertState'] === undefined ? undefined : String(queryParams['alertState']);
|
||||||
|
const dataSource = queryParams['dataSource'] === undefined ? undefined : String(queryParams['dataSource']);
|
||||||
|
|
||||||
|
return { queryString, alertState, dataSource };
|
||||||
|
};
|
||||||
|
@ -173,18 +173,25 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
if (filters.annotations && !x.meta.annotations) {
|
if (filters.annotations && !x.meta.annotations) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (filters.alerting && !x.meta.alerting) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (filters.pluginId && x.meta.id !== filters.pluginId) {
|
if (filters.pluginId && x.meta.id !== filters.pluginId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (filters.filter && !filters.filter(x)) {
|
if (filters.filter && !filters.filter(x)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (filters.type && (Array.isArray(filters.type) ? !filters.type.includes(x.type) : filters.type !== x.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!filters.all &&
|
!filters.all &&
|
||||||
x.meta.metrics !== true &&
|
x.meta.metrics !== true &&
|
||||||
x.meta.annotations !== true &&
|
x.meta.annotations !== true &&
|
||||||
x.meta.tracing !== true &&
|
x.meta.tracing !== true &&
|
||||||
x.meta.logs !== true
|
x.meta.logs !== true &&
|
||||||
|
x.meta.alerting !== true
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -91,3 +91,9 @@ export interface RuleLocation {
|
|||||||
groupName: string;
|
groupName: string;
|
||||||
ruleHash: number;
|
ruleHash: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuleFilterState {
|
||||||
|
queryString?: string;
|
||||||
|
dataSource?: string;
|
||||||
|
alertState?: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user