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 RulesFilter = (props: RulesFilerProps) => {
const newView = config.featureToggles.alertingFilterV2; const newView = config.featureToggles.alertingFilterV2;
return <Suspense>{newView ? <RulesFilterV2 {...props} /> : <RulesFilterV1 {...props} />}</Suspense>; return <Suspense>{newView ? <RulesFilterV2 /> : <RulesFilterV1 {...props} />}</Suspense>;
}; };
export default RulesFilter; export default RulesFilter;

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

Loading…
Cancel
Save