alerting/improved-filters
Gilles De Mey 2 months ago
parent 150bbc6d26
commit 5db42eadd0
  1. 2
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx
  2. 346
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx
  3. 46
      public/app/features/alerting/unified/components/rules/Filter/utils.ts
  4. 6
      public/locales/en-US/grafana.json

@ -12,7 +12,7 @@ interface RulesFilerProps {
const RulesFilter = (props: RulesFilerProps) => {
const newView = config.featureToggles.alertingFilterV2;
return <Suspense>{newView ? <RulesFilterV2 {...props} /> : <RulesFilterV1 {...props} />}</Suspense>;
return <Suspense>{newView ? <RulesFilterV2 /> : <RulesFilterV1 {...props} />}</Suspense>;
};
export default RulesFilter;

@ -1,33 +1,48 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { FormProvider, SubmitHandler, useForm, useFormContext } from 'react-hook-form';
import { useState } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import {
Button,
Combobox,
FilterInput,
Grid,
Input,
Label,
RadioButtonGroup,
Select,
Stack,
Tab,
TabsBar,
useStyles2,
} from '@grafana/ui';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
import { useRulesFilter } from '../../../hooks/useFilteredRules';
import { RuleHealth } from '../../../search/rulesSearchParser';
import { PopupCard } from '../../HoverCard';
import { RulesViewModeSelectorV2 } from './RulesViewModeSelector';
import { emptyAdvancedFilters, formAdvancedFiltersToRuleFilter, searchQueryToDefaultValues } from './utils';
type ActiveTab = 'custom' | 'saved';
type FilterForm = {
searchQuery: string;
export type AdvancedFilters = {
namespace?: string;
groupName?: string;
ruleName?: string;
ruleType?: PromRuleType | '*';
ruleState: PromAlertingRuleState | '*'; // "*" means any state
dataSourceNames: string[];
labels: string[];
ruleHealth?: RuleHealth | '*';
dashboardUid?: string;
// @TODO add support to hide / show only certain plugins
plugins?: 'show' | 'hide';
contactPoint?: string | null;
};
type SearchQueryForm = {
query: string;
};
export default function RulesFilter() {
@ -35,155 +50,212 @@ export default function RulesFilter() {
const styles = useStyles2(getStyles);
const [activeTab, setActiveTab] = useState<ActiveTab>('custom');
const { searchQuery, clearAll } = useRulesFilter();
const { searchQuery, updateFilters } = useRulesFilter();
const formContext = useForm<FilterForm>({
// this form will managed the search query string, which is updated either by the user typing in the input or by the advanced filters
const { setValue, watch, handleSubmit } = useForm<SearchQueryForm>({
defaultValues: {
searchQuery,
ruleState: '*',
query: searchQuery,
},
});
const { watch, setValue, handleSubmit, reset } = formContext;
const onSubmit: SubmitHandler<FilterForm> = (values) => {
console.log('submit', values);
const submitHandler: SubmitHandler<SearchQueryForm> = (values: SearchQueryForm) => {
console.log('search Query', values);
};
const totalQuery = watch('searchQuery');
useEffect(() => {
if (totalQuery === '') {
clearAll();
}
}, [clearAll, totalQuery]);
const handleAdvancedFilters: SubmitHandler<AdvancedFilters> = (values) => {
console.log('handleAdvancedFilters', values);
updateFilters(formAdvancedFiltersToRuleFilter(values));
};
const filterButtonLabel = t('alerting.rules-filter.filter-options.aria-label-show-filters', 'Filter');
return (
<FormProvider {...formContext}>
<form onSubmit={handleSubmit(onSubmit)} onReset={() => reset()}>
<Stack direction="row">
<FilterInput
data-testid="search-query-input"
placeholder={t(
'alerting.rules-filter.filter-options.placeholder-search-input',
'Search by name or enter filter query...'
)}
name="searchQuery"
onChange={(string) => setValue('searchQuery', string)}
value={watch('searchQuery')}
/>
<PopupCard
showOn="click"
placement="auto-end"
content={
<div className={styles.content}>
{activeTab === 'custom' && <FilterOptions />}
{/* {activeTab === 'saved' && <SavedSearches />} */}
</div>
}
header={
<TabsBar hideBorder className={styles.fixTabsMargin}>
<Tab
active={activeTab === 'custom'}
icon="filter"
label={t('alerting.rules-filter.filter-options.label-custom-filter', 'Custom filter')}
onChangeTab={() => setActiveTab('custom')}
/>
{/* <Tab
<form onSubmit={handleSubmit(submitHandler)} onReset={() => {}}>
<Stack direction="row">
<FilterInput
data-testid="search-query-input"
placeholder={t(
'alerting.rules-filter.filter-options.placeholder-search-input',
'Search by name or enter filter query...'
)}
name="searchQuery"
onChange={(string) => setValue('query', string)}
value={watch('query')}
/>
{/* the popup card is mounted inside of a portal, so we can't rely on the usual form handling mechanisms of button[type=submit] */}
<PopupCard
showOn="click"
placement="auto-end"
content={
<div className={styles.content}>
{activeTab === 'custom' && <FilterOptions onSubmit={handleAdvancedFilters} />}
{/* {activeTab === 'saved' && <SavedSearches />} */}
</div>
}
header={
<TabsBar hideBorder className={styles.fixTabsMargin}>
<Tab
active={activeTab === 'custom'}
icon="filter"
label={t('alerting.rules-filter.filter-options.label-custom-filter', 'Custom filter')}
onChangeTab={() => setActiveTab('custom')}
/>
{/* <Tab
active={activeTab === 'saved'}
icon="bookmark"
label={t('alerting.rules-filter.filter-options.label-saved-searches', 'Saved searches')}
onChangeTab={() => setActiveTab('saved')}
/> */}
</TabsBar>
}
>
<Button name="filter" icon="filter" variant="secondary" aria-label={filterButtonLabel}>
{filterButtonLabel}
</Button>
</PopupCard>
{/* show list view / group view */}
<RulesViewModeSelectorV2 />
</Stack>
</form>
</FormProvider>
</TabsBar>
}
>
<Button name="filter" icon="filter" variant="secondary" aria-label={filterButtonLabel}>
{filterButtonLabel}
</Button>
</PopupCard>
{/* show list view / group view */}
<RulesViewModeSelectorV2 />
</Stack>
</form>
);
}
const FilterOptions = () => {
const { setValue, watch } = useFormContext<FilterForm>();
interface FilterOptionsProps {
onSubmit: SubmitHandler<AdvancedFilters>;
}
const FilterOptions = ({ onSubmit }: FilterOptionsProps) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const { filterState } = useRulesFilter();
const defaultValues = searchQueryToDefaultValues(filterState);
// turn the filterState into form default values
const { handleSubmit, reset, register, control } = useForm<AdvancedFilters>({
defaultValues,
});
const submitAdvancedFilters = handleSubmit(onSubmit);
return (
<Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center">
<Label>
<Trans i18nKey="alerting.search.property.namespace">Folder / Namespace</Trans>
</Label>
<Select options={[]} onChange={() => {}} />
<Label>
<Trans i18nKey="alerting.search.property.rule-name">Alerting rule name</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.evaluation-group">Evaluation group</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.labels">Labels</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.data-source">Data source</Trans>
</Label>
<Select options={[]} onChange={() => {}} />
<Label>
<Trans i18nKey="alerting.search.property.state">State</Trans>
</Label>
<RadioButtonGroup<PromAlertingRuleState | '*'>
options={[
{ label: 'All', value: '*' },
{ label: 'Firing', value: PromAlertingRuleState.Firing },
{ label: 'Normal', value: PromAlertingRuleState.Inactive },
{ label: 'Pending', value: PromAlertingRuleState.Pending },
{ label: 'Recovering', value: PromAlertingRuleState.Recovering },
{ label: 'Unknown', value: PromAlertingRuleState.Unknown },
]}
value={watch('ruleState')}
onChange={(value) => setValue('ruleState', value)}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-type">Type</Trans>
</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Alert rule', value: 'alerting' },
{ label: 'Recording rule', value: 'recording' },
]}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-health">Health</Trans>
</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'OK', value: 'ok' },
{ label: 'No data', value: 'no_data' },
{ label: 'Error', value: 'error' },
]}
/>
</Grid>
<Stack direction="row" alignItems="center">
<Button type="reset" variant="secondary">
<Trans i18nKey="common.clear">Clear</Trans>
</Button>
<Button type="submit">
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
<form
onSubmit={submitAdvancedFilters}
onReset={() => {
reset(emptyAdvancedFilters);
submitAdvancedFilters();
}}
>
<Stack direction="column" alignItems="end" gap={2}>
<div className={styles.grid}>
<Label>
<Trans i18nKey="alerting.search.property.rule-name">Rule name</Trans>
</Label>
<Input {...register('ruleName')} />
<Label>
<Trans i18nKey="alerting.search.property.labels">Labels</Trans>
</Label>
{/* @TODO some visual label picker */}
<Input {...register('labels')} />
<Label>
<Trans i18nKey="alerting.search.property.namespace">Folder / Namespace</Trans>
</Label>
<Controller
name="namespace"
control={control}
render={({ field }) => (
<Combobox<string>
placeholder={t('alerting.rules-filter.filter-options.placeholder-namespace', 'Select namespace')}
options={[]}
onChange={field.onChange}
value={field.value}
isClearable
/>
)}
/>
<Label>
<Trans i18nKey="alerting.search.property.evaluation-group">Evaluation group</Trans>
</Label>
<Input {...register('groupName')} />
<Label>
<Trans i18nKey="alerting.search.property.data-source">Data source</Trans>
</Label>
{/* @TODO hook up data source selection */}
<Controller
name="dataSourceNames"
control={control}
render={({ field }) => <Combobox<string> options={[]} isClearable />}
/>
<Label>
<Trans i18nKey="alerting.search.property.state">State</Trans>
</Label>
<Controller
name="ruleState"
control={control}
render={({ field }) => (
<RadioButtonGroup<AdvancedFilters['ruleState']>
options={[
{ label: 'All', value: '*' },
{ label: 'Firing', value: PromAlertingRuleState.Firing },
{ label: 'Normal', value: PromAlertingRuleState.Inactive },
{ label: 'Pending', value: PromAlertingRuleState.Pending },
{ label: 'Recovering', value: PromAlertingRuleState.Recovering },
{ label: 'Unknown', value: PromAlertingRuleState.Unknown },
]}
value={field.value}
onChange={field.onChange}
/>
)}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-type">Type</Trans>
</Label>
<Controller
name="ruleType"
control={control}
render={({ field }) => (
<RadioButtonGroup<AdvancedFilters['ruleType']>
options={[
{ label: 'All', value: '*' },
{ label: 'Alert rule', value: PromRuleType.Alerting },
{ label: 'Recording rule', value: PromRuleType.Recording },
]}
value={field.value}
onChange={field.onChange}
/>
)}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-health">Health</Trans>
</Label>
<Controller
name="ruleHealth"
control={control}
render={({ field }) => (
<RadioButtonGroup<AdvancedFilters['ruleHealth']>
options={[
{ label: 'All', value: '*' },
{ label: 'OK', value: RuleHealth.Ok },
{ label: 'No data', value: RuleHealth.NoData },
{ label: 'Error', value: RuleHealth.Error },
]}
value={field.value}
onChange={field.onChange}
/>
)}
/>
</div>
<Stack direction="row" alignItems="center">
<Button type="reset" variant="secondary">
<Trans i18nKey="common.clear">Clear</Trans>
</Button>
<Button type="submit">
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
</Stack>
</Stack>
</Stack>
</form>
);
};
@ -195,5 +267,11 @@ function getStyles(theme: GrafanaTheme2) {
fixTabsMargin: css({
marginTop: theme.spacing(-1),
}),
grid: css({
display: 'grid',
gridTemplateColumns: 'repeat(2, auto)',
alignItems: 'center',
gap: theme.spacing(2),
}),
};
}

@ -0,0 +1,46 @@
import { RulesFilter } from '../../../search/rulesSearchParser';
import { AdvancedFilters } from './RulesFilter.v2';
export function formAdvancedFiltersToRuleFilter(values: AdvancedFilters): RulesFilter {
return {
freeFormWords: [],
...values,
ruleHealth: values.ruleHealth === '*' ? undefined : values.ruleHealth,
ruleState: values.ruleState === '*' ? undefined : values.ruleState,
ruleType: values.ruleType === '*' ? undefined : values.ruleType,
plugins: values.plugins === 'show' ? undefined : 'hide',
};
}
export const emptyAdvancedFilters: AdvancedFilters = {
namespace: undefined,
groupName: undefined,
ruleName: undefined,
ruleType: '*',
ruleState: '*', // "*" means any state
dataSourceNames: [],
labels: [],
ruleHealth: '*',
dashboardUid: undefined,
// @TODO add support to hide / show only certain plugins
plugins: 'show',
contactPoint: undefined,
};
export function searchQueryToDefaultValues(filterState: RulesFilter): AdvancedFilters {
return {
namespace: filterState.namespace,
groupName: filterState.groupName,
ruleName: filterState.ruleName,
ruleType: filterState.ruleType ?? '*',
ruleState: filterState.ruleState ?? '*', // "*" means any state
dataSourceNames: filterState.dataSourceNames,
labels: filterState.labels,
ruleHealth: filterState.ruleHealth ?? '*',
dashboardUid: filterState.dashboardUid,
// @TODO add support to hide / show only certain plugins
plugins: filterState.plugins ?? 'show',
contactPoint: filterState.contactPoint,
};
}

@ -2176,7 +2176,9 @@
"data-source-picker-inline-help-title-search-by-data-sources-help": "Search by data sources help",
"filter-options": {
"aria-label-show-filters": "Filter",
"label-custom-filter": "Custom filter"
"label-custom-filter": "Custom filter",
"placeholder-namespace": "Select namespace",
"placeholder-search-input": "Search by name or enter filter query..."
},
"health": "Health",
"manage-alerts": "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.",
@ -2205,7 +2207,7 @@
"labels": "Labels",
"namespace": "Folder / Namespace",
"rule-health": "Health",
"rule-name": "Alerting rule name",
"rule-name": "Rule name",
"rule-type": "Type",
"state": "State"
},

Loading…
Cancel
Save