alerting/better-search-2
Gilles De Mey 11 months ago
parent 18eb9f3b8a
commit 732ba19d05
No known key found for this signature in database
  1. 4
      packages/grafana-ui/src/components/Select/types.ts
  2. 59
      public/app/features/alerting/unified/components/rules/Filter/Options.tsx
  3. 2
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx
  4. 224
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx

@ -65,7 +65,7 @@ export interface SelectCommonProps<T> {
menuShouldPortal?: boolean; menuShouldPortal?: boolean;
/** The message to display when no options could be found */ /** The message to display when no options could be found */
noOptionsMessage?: string; noOptionsMessage?: string;
onBlur?: () => void; onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onChange: (value: SelectableValue<T>, actionMeta: ActionMeta) => {} | void; onChange: (value: SelectableValue<T>, actionMeta: ActionMeta) => {} | void;
onCloseMenu?: () => void; onCloseMenu?: () => void;
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */ /** allowCustomValue must be enabled. Function decides what to do with that custom value. */
@ -77,7 +77,7 @@ export interface SelectCommonProps<T> {
/** Callback which fires when the user scrolls to the top of the menu */ /** Callback which fires when the user scrolls to the top of the menu */
onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void; onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void;
onOpenMenu?: () => void; onOpenMenu?: () => void;
onFocus?: () => void; onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
openMenuOnFocus?: boolean; openMenuOnFocus?: boolean;
options?: Array<SelectableValue<T>>; options?: Array<SelectableValue<T>>;
placeholder?: string; placeholder?: string;

@ -0,0 +1,59 @@
import { SelectableValue } from '@grafana/data';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
import { RuleHealth } from '../../../search/rulesSearchParser';
export const AllValue = '*';
export type WithAnyOption<T> = T | '*';
export const ViewOptions: SelectableValue[] = [
{
icon: 'folder',
label: 'Grouped',
value: 'grouped',
},
{
icon: 'list-ul',
label: 'List',
value: 'list',
},
{
icon: 'heart-rate',
label: 'State',
value: 'state',
},
];
export const RuleTypeOptions: Array<SelectableValue<WithAnyOption<PromRuleType>>> = [
{
label: 'All',
value: '*',
},
{
label: 'Alert rule',
value: PromRuleType.Alerting,
},
{
label: 'Recording rule',
value: PromRuleType.Recording,
},
];
export const RuleHealthOptions: Array<SelectableValue<WithAnyOption<RuleHealth>>> = [
{ label: 'All', value: '*' },
{ label: 'Ok', value: RuleHealth.Ok },
{ label: 'No Data', value: RuleHealth.NoData },
{ label: 'Error', value: RuleHealth.Error },
];
export const RuleStateOptions: Array<SelectableValue<WithAnyOption<PromAlertingRuleState>>> = [
{ label: 'All', value: '*' },
{ label: 'Normal', value: PromAlertingRuleState.Inactive },
{ label: 'Pending', value: PromAlertingRuleState.Pending },
{ label: 'Firing', value: PromAlertingRuleState.Firing },
];
export const PluginOptions: Array<SelectableValue<'hide' | undefined>> = [
{ label: 'Show', value: undefined },
{ label: 'Hide', value: 'hide' },
];

@ -362,7 +362,7 @@ const helpStyles = (theme: GrafanaTheme2) => ({
}), }),
}); });
function usePluginsFilterStatus() { export function usePluginsFilterStatus() {
const { extensions } = useAlertingHomePageExtensions(); const { extensions } = useAlertingHomePageExtensions();
return { pluginsFilterEnabled: extensions.length > 0 }; return { pluginsFilterEnabled: extensions.length > 0 };
} }

@ -1,7 +1,9 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useCallback, useMemo, useState } from 'react'; import { filter } from 'lodash';
import { useCallback, useMemo, useState, type FocusEvent } from 'react';
import { useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { import {
Badge, Badge,
Button, Button,
@ -17,9 +19,24 @@ import {
TabsBar, TabsBar,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { RuleHealth } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { useRulesFilter } from '../../../hooks/useFilteredRules';
import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
import { PopupCard } from '../../HoverCard'; import { PopupCard } from '../../HoverCard';
import MoreButton from '../../MoreButton'; import MoreButton from '../../MoreButton';
import { MultipleDataSourcePicker } from '../MultipleDataSourcePicker';
import {
ViewOptions,
RuleHealthOptions,
RuleTypeOptions,
RuleStateOptions,
PluginOptions,
WithAnyOption,
} from './Options';
import { usePluginsFilterStatus } from './RulesFilter.v1';
type RulesFilterProps = { type RulesFilterProps = {
onClear?: () => void; onClear?: () => void;
@ -27,9 +44,24 @@ type RulesFilterProps = {
type ActiveTab = 'custom' | 'saved'; type ActiveTab = 'custom' | 'saved';
interface FormValues {
namespace?: string;
group?: string;
name?: string;
labels: string[];
dataSource: string[];
state: WithAnyOption<PromAlertingRuleState>;
type: WithAnyOption<'alerting' | 'recording'>;
health: WithAnyOption<RuleHealth>;
dashboardUID?: string;
plugins?: 'hide'; // @TODO support selecting one or more plugin sources to filter by
}
export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) { export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { searchQuery } = useRulesFilter();
const [activeTab, setActiveTab] = useState<ActiveTab>('custom'); const [activeTab, setActiveTab] = useState<ActiveTab>('custom');
const [queryParams, updateQueryParams] = useURLSearchParams();
const filterOptions = useMemo(() => { const filterOptions = useMemo(() => {
return ( return (
@ -65,64 +97,141 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
}, [activeTab, styles.content, styles.fixTabsMargin]); }, [activeTab, styles.content, styles.fixTabsMargin]);
return ( return (
<Stack direction="column" gap={0}> <form>
<Label>Search</Label> <Stack direction="row" alignItems="end">
<Stack direction="row"> <Stack direction="column" gap={0} flex={1}>
<Input prefix={filterOptions} /> <Label>Search</Label>
<Input prefix={filterOptions} defaultValue={searchQuery} />
</Stack>
<Button type="submit" variant="secondary">
Search
</Button>
<Stack direction="column" gap={0}>
<Label>View as</Label>
<RadioButtonGroup
options={ViewOptions}
value={queryParams.get('view') ?? ViewOptions[0].value}
onChange={(view: string) => updateQueryParams({ view })}
/>
</Stack>
</Stack> </Stack>
</Stack> </form>
); );
} }
const FilterOptions = () => { const FilterOptions = () => {
const { pluginsFilterEnabled } = usePluginsFilterStatus();
const { updateFilters, filterState } = useRulesFilter();
const { register, handleSubmit, setValue, reset, resetField, watch, getValues } = useForm<FormValues>({
defaultValues: {
state: filterState.ruleState ?? '*',
type: filterState.ruleType ?? '*',
health: filterState.ruleHealth ?? '*',
plugins: filterState.plugins ?? 'hide',
dataSource: filterState.dataSourceNames ?? [],
labels: filterState.labels ?? [],
},
});
const onSubmit = (values: FormValues) => {
updateFilters({
groupName: values.group,
namespace: values.namespace,
dataSourceNames: values.dataSource,
freeFormWords: [],
labels: [],
ruleName: values.name,
dashboardUid: values.dashboardUID,
plugins: values.plugins,
ruleState: anyValueToUndefined(values.state),
ruleHealth: anyValueToUndefined(values.health),
ruleType: anyValueToUndefined(values.type),
});
};
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings, action: 'add' | 'remove') => {
if (action === 'add') {
const existingValue = getValues('dataSource') ?? [];
setValue('dataSource', existingValue.concat(dataSourceValue.name));
} else if (action === 'remove') {
const existingValue = getValues('dataSource') ?? [];
setValue('dataSource', filter(existingValue, dataSourceValue.name));
}
};
return ( return (
<Stack direction="column" alignItems="end" gap={2}> <form onSubmit={handleSubmit(onSubmit)}>
<Grid columns={2} gap={2} alignItems="center"> <Stack direction="column" alignItems="end" gap={2}>
<Label>Folder / Namespace</Label> <Grid columns={2} gap={2} alignItems="center">
<Select options={[]} onChange={() => {}}></Select> <Label>Folder / Namespace</Label>
<Label>Alert rule name</Label> <Select {...register('namespace')} options={[]} onBlur={stopPropagation} />
<Input /> <Label>Rule name</Label>
<Label>Evaluation group</Label> <Input {...register('name')} onBlur={stopPropagation} />
<Input /> <Label>Evaluation group</Label>
<Label>Labels</Label> <Input {...register('group')} onBlur={stopPropagation} />
<Input /> <Label>Labels</Label>
<Label>Data source</Label> <Input {...register('labels')} onBlur={stopPropagation} />
<Select options={[]} onChange={() => {}}></Select> <Label>Data source</Label>
<Label>State</Label> <MultipleDataSourcePicker
<RadioButtonGroup alerting
value={'*'} noDefault
options={[ placeholder="All data sources"
{ label: 'All', value: '*' }, current={watch('dataSource')}
{ label: 'Normal', value: 'normal' }, onChange={handleDataSourceChange}
{ label: 'Pending', value: 'pending' }, onClear={() => resetField('dataSource')}
{ label: 'Firing', value: 'firing' }, onBlur={stopPropagation}
]} />
/> <Label>Dashboard</Label>
<Label>Type</Label> <Select {...register('dashboardUID')} options={[]} onBlur={stopPropagation} />
<RadioButtonGroup {pluginsFilterEnabled && (
value={'*'} <div>
options={[ <Label>From plugin</Label>
{ label: 'All', value: '*' }, <RadioButtonGroup
{ label: 'Alert rule', value: 'alerting' }, options={PluginOptions}
{ label: 'Recording rule', value: 'recording' }, {...register('plugins')}
]} value={watch('plugins')}
/> onChange={(value) => {
<Label>Health</Label> setValue('plugins', value);
<RadioButtonGroup }}
value={'*'} />
options={[ </div>
{ label: 'All', value: '*' }, )}
{ label: 'OK', value: 'ok' }, <Label>State</Label>
{ label: 'No data', value: 'no_data' }, <RadioButtonGroup
{ label: 'Error', value: 'error' }, options={RuleStateOptions}
]} {...register('state')}
/> value={watch('state')}
</Grid> onChange={(value) => {
<Stack direction="row" alignItems="center"> setValue('state', value);
<Button variant="secondary">Clear</Button> }}
<Button>Apply</Button> />
<Label>Type</Label>
<RadioButtonGroup
options={RuleTypeOptions}
{...register('type')}
value={watch('type')}
onChange={(value) => {
setValue('type', value);
}}
/>
<Label>Health</Label>
<RadioButtonGroup
options={RuleHealthOptions}
{...register('health')}
value={watch('health')}
onChange={(value) => {
setValue('health', value);
}}
/>
</Grid>
<Stack direction="row" alignItems="center">
<Button variant="secondary" onClick={() => reset()}>
Clear
</Button>
<Button type="submit">Apply</Button>
</Stack>
</Stack> </Stack>
</Stack> </form>
); );
}; };
@ -195,3 +304,12 @@ function getStyles(theme: GrafanaTheme2) {
}), }),
}; };
} }
function anyValueToUndefined<T extends string>(input: T): Omit<T, '*'> | undefined {
return String(input) === '*' ? undefined : input;
}
// we'll need this util function to prevent onBlur of the inputs to trigger closing the interactive card
function stopPropagation(e: FocusEvent) {
e.stopPropagation();
}

Loading…
Cancel
Save