search WIP fix

alerting/list-view-v2-minor-fixes
Gilles De Mey 4 months ago
parent 59d87fe3f1
commit 2c2b8e2f1e
No known key found for this signature in database
  1. 181
      public/app/features/alerting/unified/rule-list/components/RulesSourcePicker.tsx
  2. 54
      public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts

@ -0,0 +1,181 @@
import { useState } from 'react';
import { PopValueActionMeta, RemoveValueActionMeta } from 'react-select';
import {
DataSourceInstanceSettings,
SelectableValue,
getDataSourceUID,
isUnsignedPluginSignature,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui';
import { isDataSourceManagingAlerts } from '../../utils/datasource';
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(isDataSourceManagingAlerts).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) => !isDataSourceManagingAlerts(ds))
.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 (
<Stack alignItems="center" justifyContent="space-between">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
</Stack>
);
}
return o.label || '';
}}
/>
</div>
);
};

@ -1,7 +1,7 @@
import { AsyncIterableX, empty, from } from 'ix/asynciterable';
import { merge } from 'ix/asynciterable/merge';
import { filter, flatMap, map } from 'ix/asynciterable/operators';
import { compact } from 'lodash';
import { compact, includes, isEmpty } from 'lodash';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import {
@ -19,7 +19,7 @@ import {
import { RulesFilter } from '../../search/rulesSearchParser';
import { labelsMatchMatchers } from '../../utils/alertmanager';
import { Annotation } from '../../utils/constants';
import { getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
import { getDataSourceByUid, getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
import { parseMatcher } from '../../utils/matchers';
import { prometheusRuleType } from '../../utils/rules';
@ -52,15 +52,9 @@ export function useFilteredRulesIteratorProvider() {
const getFilteredRulesIterator = (filterState: RulesFilter, groupLimit: number): AsyncIterableX<RuleWithOrigin> => {
const normalizedFilterState = normalizeFilterState(filterState);
const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length);
const ruleSourcesToFetchFrom = filterState.dataSourceNames.length
? filterState.dataSourceNames.map<DataSourceRulesSourceIdentifier>((ds) => ({
name: ds,
uid: getDatasourceAPIUid(ds),
ruleSourceType: 'datasource',
}))
: allExternalRulesSources;
// create the iterable sequence for Grafana managed implementation
const grafanaIterator = from(grafanaGroupsGenerator(groupLimit)).pipe(
filter((group) => groupFilter(group, normalizedFilterState)),
flatMap((group) => group.rules.map((rule) => [group, rule] as const)),
@ -68,14 +62,20 @@ export function useFilteredRulesIteratorProvider() {
map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule))
);
const sourceIterables = ruleSourcesToFetchFrom.map((ds) => {
const generator = prometheusGroupsGenerator(ds, groupLimit);
return from(generator).pipe(map((group) => [ds, group] as const));
// check filters for potential data source filter to we don't pull from all data sources
const externalRulesSourcesToFetchFrom = hasDataSourceFilterActive
? getRulesSourcesFromFilter(filterState)
: allExternalRulesSources;
// create the iterable sequence for upstream Prometheus / Mimir managed implementation
const prometheusRulesSourceIterables = externalRulesSourcesToFetchFrom.map((dataSourceIdentifier) => {
const generator = prometheusGroupsGenerator(dataSourceIdentifier, groupLimit);
return from(generator).pipe(map((group) => [dataSourceIdentifier, group] as const));
});
// if we have no prometheus data sources, use an empty async iterable
const source = sourceIterables.at(0) ?? empty();
const otherIterables = sourceIterables.slice(1);
const source = isEmpty(prometheusRulesSourceIterables) ? empty() : prometheusRulesSourceIterables[0];
const otherIterables = prometheusRulesSourceIterables.slice(1);
const dataSourcesIterator = merge(source, ...otherIterables).pipe(
filter(([_, group]) => groupFilter(group, normalizedFilterState)),
@ -90,6 +90,30 @@ export function useFilteredRulesIteratorProvider() {
return { getFilteredRulesIterator };
}
// find all data sources that the user might want to filter by, only allow Prometheus and Loki data source types
const SUPPORTED_RULES_SOURCE_TYPES = ['loki', 'prometheus'];
function getRulesSourcesFromFilter(filter: RulesFilter): DataSourceRulesSourceIdentifier[] {
return filter.dataSourceNames.reduce<DataSourceRulesSourceIdentifier[]>((acc, dataSourceName) => {
// since "getDatasourceAPIUid" can throw we'll omit any non-existing data sources
try {
const uid = getDatasourceAPIUid(dataSourceName);
const type = getDataSourceByUid(uid)?.type;
if (!includes(SUPPORTED_RULES_SOURCE_TYPES, type)) {
return acc;
}
acc.push({
name: dataSourceName,
uid,
ruleSourceType: 'datasource',
});
} catch {}
return acc;
}, []);
}
function mapRuleToRuleWithOrigin(
rulesSource: DataSourceRulesSourceIdentifier,
group: PromRuleGroupDTO,

Loading…
Cancel
Save