mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Support filtering rules by multiple datasources (#64355)
* Support having a datasources array in the rules filter * Fix tests * Display a MultiplePicker for filtering datasources * Fix tests * Refactor as MultipleDataSourcePicker as FC * Make select box wider * Remove FC from component definition * Display ds options in groups based on whether they manage/don't manage rules * Change dropdown texts and add help info
This commit is contained in:
parent
b3d0c39f08
commit
43bbe567e7
@ -0,0 +1,181 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { PopValueActionMeta, RemoveValueActionMeta } from 'react-select';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
getDataSourceUID,
|
||||||
|
isUnsignedPluginSignature,
|
||||||
|
SelectableValue,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { getDataSourceSrv, DataSourcePickerState, DataSourcePickerProps } from '@grafana/runtime';
|
||||||
|
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
||||||
|
import { ActionMeta, HorizontalGroup, PluginSignatureBadge, MultiSelect } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface MultipleDataSourcePickerProps extends Omit<DataSourcePickerProps, 'onChange' | 'current'> {
|
||||||
|
onChange: (ds: DataSourceInstanceSettings, action: 'add' | 'remove') => void;
|
||||||
|
current: string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) => {
|
||||||
|
const dataSourceSrv = getDataSourceSrv();
|
||||||
|
|
||||||
|
const [state, setState] = useState<DataSourcePickerState>();
|
||||||
|
|
||||||
|
const onChange = (items: Array<SelectableValue<string>>, actionMeta: ActionMeta) => {
|
||||||
|
if (actionMeta.action === 'clear' && props.onClear) {
|
||||||
|
props.onClear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedItem = items[items.length - 1];
|
||||||
|
|
||||||
|
let dataSourceName, action: 'add' | 'remove';
|
||||||
|
|
||||||
|
if (actionMeta.action === 'pop-value' || actionMeta.action === 'remove-value') {
|
||||||
|
const castedActionMeta:
|
||||||
|
| RemoveValueActionMeta<SelectableValue<string>>
|
||||||
|
| PopValueActionMeta<SelectableValue<string>> = actionMeta;
|
||||||
|
dataSourceName = castedActionMeta.removedValue?.value;
|
||||||
|
action = 'remove';
|
||||||
|
} else {
|
||||||
|
dataSourceName = selectedItem.value;
|
||||||
|
action = 'add';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dsSettings = dataSourceSrv.getInstanceSettings(dataSourceName);
|
||||||
|
|
||||||
|
if (dsSettings) {
|
||||||
|
props.onChange(dsSettings, action);
|
||||||
|
setState({ error: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentValue = (): Array<SelectableValue<string>> | undefined => {
|
||||||
|
const { current, hideTextValue, noDefault } = props;
|
||||||
|
if (!current && noDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current?.map((dataSourceName: string) => {
|
||||||
|
const ds = dataSourceSrv.getInstanceSettings(dataSourceName);
|
||||||
|
if (ds) {
|
||||||
|
return {
|
||||||
|
label: ds.name.slice(0, 37),
|
||||||
|
value: ds.name,
|
||||||
|
imgUrl: ds.meta.info.logos.small,
|
||||||
|
hideText: hideTextValue,
|
||||||
|
meta: ds.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = getDataSourceUID(dataSourceName);
|
||||||
|
|
||||||
|
if (uid === ExpressionDatasourceRef.uid || uid === ExpressionDatasourceRef.name) {
|
||||||
|
return { label: uid, value: uid, hideText: hideTextValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: (uid ?? 'no name') + ' - not found',
|
||||||
|
value: uid ?? undefined,
|
||||||
|
imgUrl: '',
|
||||||
|
hideText: hideTextValue,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataSourceOptions = () => {
|
||||||
|
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const dataSources = dataSourceSrv.getList({
|
||||||
|
alerting,
|
||||||
|
tracing,
|
||||||
|
metrics,
|
||||||
|
logs,
|
||||||
|
dashboard,
|
||||||
|
mixed,
|
||||||
|
variables,
|
||||||
|
annotations,
|
||||||
|
pluginId,
|
||||||
|
filter,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertManagingDs = dataSources
|
||||||
|
.filter((ds) => ds.jsonData.manageAlerts)
|
||||||
|
.map((ds) => ({
|
||||||
|
value: ds.name,
|
||||||
|
label: `${ds.name}${ds.isDefault ? ' (default)' : ''}`,
|
||||||
|
imgUrl: ds.meta.info.logos.small,
|
||||||
|
meta: ds.meta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nonAlertManagingDs = dataSources
|
||||||
|
.filter((ds) => !ds.jsonData.manageAlerts)
|
||||||
|
.map((ds) => ({
|
||||||
|
value: ds.name,
|
||||||
|
label: `${ds.name}${ds.isDefault ? ' (default)' : ''}`,
|
||||||
|
imgUrl: ds.meta.info.logos.small,
|
||||||
|
meta: ds.meta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const groupedOptions = [
|
||||||
|
{ label: 'Data sources with configured alert rules', options: alertManagingDs, expanded: true },
|
||||||
|
{ label: 'Other data sources', options: nonAlertManagingDs, expanded: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
return groupedOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
autoFocus,
|
||||||
|
onBlur,
|
||||||
|
onClear,
|
||||||
|
openMenuOnFocus,
|
||||||
|
placeholder,
|
||||||
|
width,
|
||||||
|
inputId,
|
||||||
|
disabled = false,
|
||||||
|
isLoading = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const options = getDataSourceOptions();
|
||||||
|
const value = getCurrentValue();
|
||||||
|
const isClearable = typeof onClear === 'function';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid={selectors.components.DataSourcePicker.container}>
|
||||||
|
<MultiSelect
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid={selectors.components.DataSourcePicker.inputV2}
|
||||||
|
inputId={inputId || 'data-source-picker'}
|
||||||
|
className="ds-picker select-container"
|
||||||
|
isClearable={isClearable}
|
||||||
|
backspaceRemovesValue={true}
|
||||||
|
onChange={onChange}
|
||||||
|
options={options}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
width={width}
|
||||||
|
openMenuOnFocus={openMenuOnFocus}
|
||||||
|
maxMenuHeight={500}
|
||||||
|
placeholder={placeholder}
|
||||||
|
noOptionsMessage="No datasources found"
|
||||||
|
value={value ?? []}
|
||||||
|
invalid={Boolean(state?.error) || Boolean(props.invalid)}
|
||||||
|
getOptionLabel={(o) => {
|
||||||
|
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
|
||||||
|
return (
|
||||||
|
<HorizontalGroup align="center" justify="space-between" height="auto">
|
||||||
|
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return o.label || '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -19,6 +19,14 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('./MultipleDataSourcePicker', () => {
|
||||||
|
const original = jest.requireActual('./MultipleDataSourcePicker');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
MultipleDataSourcePicker: () => <></>,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv({}));
|
setDataSourceSrv(new MockDataSourceSrv({}));
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
|
@ -4,8 +4,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { DataSourcePicker, logInfo } from '@grafana/runtime';
|
import { logInfo } from '@grafana/runtime';
|
||||||
import { Button, Field, Icon, Input, Label, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
import { Button, Field, Icon, Input, Label, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
@ -15,6 +15,8 @@ import { RuleHealth } from '../../search/rulesSearchParser';
|
|||||||
import { alertStateToReadable } from '../../utils/rules';
|
import { alertStateToReadable } from '../../utils/rules';
|
||||||
import { HoverCard } from '../HoverCard';
|
import { HoverCard } from '../HoverCard';
|
||||||
|
|
||||||
|
import { MultipleDataSourcePicker } from './MultipleDataSourcePicker';
|
||||||
|
|
||||||
const ViewOptions: SelectableValue[] = [
|
const ViewOptions: SelectableValue[] = [
|
||||||
{
|
{
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
@ -77,13 +79,22 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
|
|||||||
setValue('searchQuery', searchQuery);
|
setValue('searchQuery', searchQuery);
|
||||||
}, [searchQuery, setValue]);
|
}, [searchQuery, setValue]);
|
||||||
|
|
||||||
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => {
|
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings, action: 'add' | 'remove') => {
|
||||||
updateFilters({ ...filterState, dataSourceName: dataSourceValue.name });
|
const dataSourceNames =
|
||||||
|
action === 'add'
|
||||||
|
? [...filterState.dataSourceNames].concat([dataSourceValue.name])
|
||||||
|
: filterState.dataSourceNames.filter((name) => name !== dataSourceValue.name);
|
||||||
|
|
||||||
|
updateFilters({
|
||||||
|
...filterState,
|
||||||
|
dataSourceNames,
|
||||||
|
});
|
||||||
|
|
||||||
setFilterKey((key) => key + 1);
|
setFilterKey((key) => key + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearDataSource = () => {
|
const clearDataSource = () => {
|
||||||
updateFilters({ ...filterState, dataSourceName: undefined });
|
updateFilters({ ...filterState, dataSourceNames: [] });
|
||||||
setFilterKey((key) => key + 1);
|
setFilterKey((key) => key + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,17 +130,43 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Stack direction="column" gap={1}>
|
<Stack direction="column" gap={1}>
|
||||||
<Stack direction="row" gap={1}>
|
<Stack direction="row" gap={1}>
|
||||||
<Field className={styles.dsPickerContainer} label="Search by data source">
|
<Field
|
||||||
<DataSourcePicker
|
className={styles.dsPickerContainer}
|
||||||
|
label={
|
||||||
|
<Label htmlFor="data-source-picker">
|
||||||
|
<Stack gap={0.5}>
|
||||||
|
<span>Search by data sources</span>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Data sources containing configured alert rules are Mimir or Loki data sources where alert
|
||||||
|
rules are stored and evaluated in the data source itself.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In these data sources, you can select Manage alerts via Alerting UI to be able to manage these
|
||||||
|
alert rules in the Grafana UI as well as in the data source where they were configured.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name="info-circle" size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MultipleDataSourcePicker
|
||||||
key={dataSourceKey}
|
key={dataSourceKey}
|
||||||
alerting
|
alerting
|
||||||
noDefault
|
noDefault
|
||||||
placeholder="All data sources"
|
placeholder="All data sources"
|
||||||
current={filterState.dataSourceName}
|
current={filterState.dataSourceNames}
|
||||||
onChange={handleDataSourceChange}
|
onChange={handleDataSourceChange}
|
||||||
onClear={clearDataSource}
|
onClear={clearDataSource}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>State</Label>
|
<Label>State</Label>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
@ -215,7 +252,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
margin-bottom: ${theme.spacing(1)};
|
margin-bottom: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
dsPickerContainer: css`
|
dsPickerContainer: css`
|
||||||
width: 250px;
|
width: 550px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`,
|
`,
|
||||||
@ -236,7 +273,7 @@ function SearchQueryHelp() {
|
|||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
<div>Filter type</div>
|
<div>Filter type</div>
|
||||||
<div>Expression</div>
|
<div>Expression</div>
|
||||||
<HelpRow title="Datasource" expr="datasource:mimir" />
|
<HelpRow title="Datasources" expr="datasource:mimir datasource:prometheus" />
|
||||||
<HelpRow title="Folder/Namespace" expr="namespace:global" />
|
<HelpRow title="Folder/Namespace" expr="namespace:global" />
|
||||||
<HelpRow title="Group" expr="group:cpu-usage" />
|
<HelpRow title="Group" expr="group:cpu-usage" />
|
||||||
<HelpRow title="Rule" expr='rule:"cpu 80%"' />
|
<HelpRow title="Rule" expr='rule:"cpu 80%"' />
|
||||||
|
@ -160,7 +160,7 @@ describe('filterRules', function () {
|
|||||||
groups: [mockCombinedRuleGroup('Resources usage group', rules)],
|
groups: [mockCombinedRuleGroup('Resources usage group', rules)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const filtered = filterRules([ns], getFilter({ dataSourceName: 'loki' }));
|
const filtered = filterRules([ns], getFilter({ dataSourceNames: ['loki'] }));
|
||||||
|
|
||||||
expect(filtered[0].groups[0].rules).toHaveLength(1);
|
expect(filtered[0].groups[0].rules).toHaveLength(1);
|
||||||
expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low');
|
expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low');
|
||||||
|
@ -52,7 +52,7 @@ export function useRulesFilter() {
|
|||||||
// Existing query filters takes precedence over legacy ones
|
// Existing query filters takes precedence over legacy ones
|
||||||
updateFilters(
|
updateFilters(
|
||||||
produce(filterState, (draft) => {
|
produce(filterState, (draft) => {
|
||||||
draft.dataSourceName ??= legacyFilters.dataSource;
|
draft.dataSourceNames ??= legacyFilters.dataSource ? [legacyFilters.dataSource] : [];
|
||||||
if (legacyFilters.alertState && isPromAlertingRuleState(legacyFilters.alertState)) {
|
if (legacyFilters.alertState && isPromAlertingRuleState(legacyFilters.alertState)) {
|
||||||
draft.ruleState ??= legacyFilters.alertState;
|
draft.ruleState ??= legacyFilters.alertState;
|
||||||
}
|
}
|
||||||
@ -90,14 +90,14 @@ const ufuzzy = new uFuzzy({
|
|||||||
|
|
||||||
export const filterRules = (
|
export const filterRules = (
|
||||||
namespaces: CombinedRuleNamespace[],
|
namespaces: CombinedRuleNamespace[],
|
||||||
filterState: RulesFilter = { labels: [], freeFormWords: [] }
|
filterState: RulesFilter = { dataSourceNames: [], labels: [], freeFormWords: [] }
|
||||||
): CombinedRuleNamespace[] => {
|
): CombinedRuleNamespace[] => {
|
||||||
let filteredNamespaces = namespaces;
|
let filteredNamespaces = namespaces;
|
||||||
|
|
||||||
const dataSourceFilter = filterState.dataSourceName;
|
const dataSourceFilter = filterState.dataSourceNames;
|
||||||
if (dataSourceFilter) {
|
if (dataSourceFilter.length) {
|
||||||
filteredNamespaces = filteredNamespaces.filter(({ rulesSource }) =>
|
filteredNamespaces = filteredNamespaces.filter(({ rulesSource }) =>
|
||||||
isCloudRulesSource(rulesSource) ? rulesSource.name === dataSourceFilter : true
|
isCloudRulesSource(rulesSource) ? dataSourceFilter.includes(rulesSource.name) : true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ const reduceGroups = (filterState: RulesFilter) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const doesNotQueryDs = isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filterState);
|
const doesNotQueryDs = isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filterState);
|
||||||
if (filterState.dataSourceName && doesNotQueryDs) {
|
if (filterState.dataSourceNames?.length && doesNotQueryDs) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ function looseParseMatcher(matcherQuery: string): Matcher | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: RulesFilter): boolean => {
|
const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: RulesFilter): boolean => {
|
||||||
if (!filterState.dataSourceName) {
|
if (!filterState.dataSourceNames?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +232,6 @@ const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: Rules
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const ds = getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
const ds = getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||||
return ds?.name === filterState.dataSourceName;
|
return ds?.name && filterState?.dataSourceNames?.includes(ds.name);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
describe('getSearchFilterFromQuery', () => {
|
describe('getSearchFilterFromQuery', () => {
|
||||||
it.each(['datasource:prometheus'])('should parse data source filter from "%s" query', (query) => {
|
it.each(['datasource:prometheus'])('should parse data source filter from "%s" query', (query) => {
|
||||||
const filter = getSearchFilterFromQuery(query);
|
const filter = getSearchFilterFromQuery(query);
|
||||||
expect(filter.dataSourceName).toBe('prometheus');
|
expect(filter.dataSourceNames).toEqual(['prometheus']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(['namespace:integrations-node'])('should parse namespace filter from "%s" query', (query) => {
|
it.each(['namespace:integrations-node'])('should parse namespace filter from "%s" query', (query) => {
|
||||||
@ -79,7 +79,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
'datasource:"prom dev" namespace:"node one" label:"team=frontend us" group:"cpu alerts" rule:"cpu failure"';
|
'datasource:"prom dev" namespace:"node one" label:"team=frontend us" group:"cpu alerts" rule:"cpu failure"';
|
||||||
const filter = getSearchFilterFromQuery(query);
|
const filter = getSearchFilterFromQuery(query);
|
||||||
|
|
||||||
expect(filter.dataSourceName).toBe('prom dev');
|
expect(filter.dataSourceNames).toEqual(['prom dev']);
|
||||||
expect(filter.namespace).toBe('node one');
|
expect(filter.namespace).toBe('node one');
|
||||||
expect(filter.labels).toContain('team=frontend us');
|
expect(filter.labels).toContain('team=frontend us');
|
||||||
expect(filter.groupName).toContain('cpu alerts');
|
expect(filter.groupName).toContain('cpu alerts');
|
||||||
@ -91,7 +91,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
'datasource:prom::dev/linux>>; namespace:"[{node}] (#20+)" label:_region=apac|emea\\nasa group:$20.00%$ rule:"cpu!! & memory.,?"';
|
'datasource:prom::dev/linux>>; namespace:"[{node}] (#20+)" label:_region=apac|emea\\nasa group:$20.00%$ rule:"cpu!! & memory.,?"';
|
||||||
const filter = getSearchFilterFromQuery(query);
|
const filter = getSearchFilterFromQuery(query);
|
||||||
|
|
||||||
expect(filter.dataSourceName).toBe('prom::dev/linux>>;');
|
expect(filter.dataSourceNames).toEqual(['prom::dev/linux>>;']);
|
||||||
expect(filter.namespace).toBe('[{node}] (#20+)');
|
expect(filter.namespace).toBe('[{node}] (#20+)');
|
||||||
expect(filter.labels).toContain('_region=apac|emea\\nasa');
|
expect(filter.labels).toContain('_region=apac|emea\\nasa');
|
||||||
expect(filter.groupName).toContain('$20.00%$');
|
expect(filter.groupName).toContain('$20.00%$');
|
||||||
@ -110,7 +110,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
const query = 'datasource:prometheus utilization label:team cpu';
|
const query = 'datasource:prometheus utilization label:team cpu';
|
||||||
const filter = getSearchFilterFromQuery(query);
|
const filter = getSearchFilterFromQuery(query);
|
||||||
|
|
||||||
expect(filter.dataSourceName).toBe('prometheus');
|
expect(filter.dataSourceNames).toEqual(['prometheus']);
|
||||||
expect(filter.labels).toContain('team');
|
expect(filter.labels).toContain('team');
|
||||||
expect(filter.freeFormWords).toContain('utilization');
|
expect(filter.freeFormWords).toContain('utilization');
|
||||||
expect(filter.freeFormWords).toContain('cpu');
|
expect(filter.freeFormWords).toContain('cpu');
|
||||||
@ -130,7 +130,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
it('should apply filters to an empty query', () => {
|
it('should apply filters to an empty query', () => {
|
||||||
const filter = getFilter({
|
const filter = getFilter({
|
||||||
freeFormWords: ['cpu', 'eighty'],
|
freeFormWords: ['cpu', 'eighty'],
|
||||||
dataSourceName: 'Mimir Dev',
|
dataSourceNames: ['Mimir Dev'],
|
||||||
namespace: '/etc/prometheus',
|
namespace: '/etc/prometheus',
|
||||||
labels: ['team', 'region=apac'],
|
labels: ['team', 'region=apac'],
|
||||||
groupName: 'cpu-usage',
|
groupName: 'cpu-usage',
|
||||||
@ -149,7 +149,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
|
|
||||||
it('should update filters in existing query', () => {
|
it('should update filters in existing query', () => {
|
||||||
const filter = getFilter({
|
const filter = getFilter({
|
||||||
dataSourceName: 'Mimir Dev',
|
dataSourceNames: ['Mimir Dev'],
|
||||||
namespace: '/etc/prometheus',
|
namespace: '/etc/prometheus',
|
||||||
labels: ['team', 'region=apac'],
|
labels: ['team', 'region=apac'],
|
||||||
groupName: 'cpu-usage',
|
groupName: 'cpu-usage',
|
||||||
@ -166,7 +166,7 @@ describe('Alert rules searchParser', () => {
|
|||||||
|
|
||||||
it('should preserve the order of parameters when updating', () => {
|
it('should preserve the order of parameters when updating', () => {
|
||||||
const filter = getFilter({
|
const filter = getFilter({
|
||||||
dataSourceName: 'Mimir Dev',
|
dataSourceNames: ['Mimir Dev'],
|
||||||
namespace: '/etc/prometheus',
|
namespace: '/etc/prometheus',
|
||||||
labels: ['region=emea'],
|
labels: ['region=emea'],
|
||||||
groupName: 'cpu-usage',
|
groupName: 'cpu-usage',
|
||||||
|
@ -17,7 +17,7 @@ export interface RulesFilter {
|
|||||||
ruleName?: string;
|
ruleName?: string;
|
||||||
ruleState?: PromAlertingRuleState;
|
ruleState?: PromAlertingRuleState;
|
||||||
ruleType?: PromRuleType;
|
ruleType?: PromRuleType;
|
||||||
dataSourceName?: string;
|
dataSourceNames: string[];
|
||||||
labels: string[];
|
labels: string[];
|
||||||
ruleHealth?: RuleHealth;
|
ruleHealth?: RuleHealth;
|
||||||
}
|
}
|
||||||
@ -42,10 +42,10 @@ export enum RuleHealth {
|
|||||||
|
|
||||||
// Define how to map parsed tokens into the filter object
|
// Define how to map parsed tokens into the filter object
|
||||||
export function getSearchFilterFromQuery(query: string): RulesFilter {
|
export function getSearchFilterFromQuery(query: string): RulesFilter {
|
||||||
const filter: RulesFilter = { labels: [], freeFormWords: [] };
|
const filter: RulesFilter = { labels: [], freeFormWords: [], dataSourceNames: [] };
|
||||||
|
|
||||||
const tokenToFilterMap: QueryFilterMapper = {
|
const tokenToFilterMap: QueryFilterMapper = {
|
||||||
[terms.DataSourceToken]: (value) => (filter.dataSourceName = value),
|
[terms.DataSourceToken]: (value) => filter.dataSourceNames.push(value),
|
||||||
[terms.NameSpaceToken]: (value) => (filter.namespace = value),
|
[terms.NameSpaceToken]: (value) => (filter.namespace = value),
|
||||||
[terms.GroupToken]: (value) => (filter.groupName = value),
|
[terms.GroupToken]: (value) => (filter.groupName = value),
|
||||||
[terms.RuleToken]: (value) => (filter.ruleName = value),
|
[terms.RuleToken]: (value) => (filter.ruleName = value),
|
||||||
@ -68,8 +68,8 @@ export function applySearchFilterToQuery(query: string, filter: RulesFilter): st
|
|||||||
|
|
||||||
// Convert filter object into an array
|
// Convert filter object into an array
|
||||||
// It allows to pick filters from the array in the same order as they were applied in the original query
|
// It allows to pick filters from the array in the same order as they were applied in the original query
|
||||||
if (filter.dataSourceName) {
|
if (filter.dataSourceNames) {
|
||||||
filterStateArray.push({ type: terms.DataSourceToken, value: filter.dataSourceName });
|
filterStateArray.push(...filter.dataSourceNames.map((t) => ({ type: terms.DataSourceToken, value: t })));
|
||||||
}
|
}
|
||||||
if (filter.namespace) {
|
if (filter.namespace) {
|
||||||
filterStateArray.push({ type: terms.NameSpaceToken, value: filter.namespace });
|
filterStateArray.push({ type: terms.NameSpaceToken, value: filter.namespace });
|
||||||
|
@ -4,6 +4,7 @@ export function getFilter(filter: Partial<RulesFilter>): RulesFilter {
|
|||||||
return {
|
return {
|
||||||
freeFormWords: [],
|
freeFormWords: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
|
dataSourceNames: [],
|
||||||
...filter,
|
...filter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user