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;
/** The message to display when no options could be found */
noOptionsMessage?: string;
onBlur?: () => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onChange: (value: SelectableValue<T>, actionMeta: ActionMeta) => {} | void;
onCloseMenu?: () => void;
/** 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 */
onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void;
onOpenMenu?: () => void;
onFocus?: () => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
openMenuOnFocus?: boolean;
options?: Array<SelectableValue<T>>;
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();
return { pluginsFilterEnabled: extensions.length > 0 };
}

@ -1,7 +1,9 @@
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 {
Badge,
Button,
@ -17,9 +19,24 @@ import {
TabsBar,
useStyles2,
} 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 MoreButton from '../../MoreButton';
import { MultipleDataSourcePicker } from '../MultipleDataSourcePicker';
import {
ViewOptions,
RuleHealthOptions,
RuleTypeOptions,
RuleStateOptions,
PluginOptions,
WithAnyOption,
} from './Options';
import { usePluginsFilterStatus } from './RulesFilter.v1';
type RulesFilterProps = {
onClear?: () => void;
@ -27,9 +44,24 @@ type RulesFilterProps = {
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) {
const styles = useStyles2(getStyles);
const { searchQuery } = useRulesFilter();
const [activeTab, setActiveTab] = useState<ActiveTab>('custom');
const [queryParams, updateQueryParams] = useURLSearchParams();
const filterOptions = useMemo(() => {
return (
@ -65,64 +97,141 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
}, [activeTab, styles.content, styles.fixTabsMargin]);
return (
<Stack direction="column" gap={0}>
<Label>Search</Label>
<Stack direction="row">
<Input prefix={filterOptions} />
<form>
<Stack direction="row" alignItems="end">
<Stack direction="column" gap={0} flex={1}>
<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>
</form>
);
}
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 (
<Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center">
<Label>Folder / Namespace</Label>
<Select options={[]} onChange={() => {}}></Select>
<Label>Alert rule name</Label>
<Input />
<Label>Evaluation group</Label>
<Input />
<Label>Labels</Label>
<Input />
<Label>Data source</Label>
<Select options={[]} onChange={() => {}}></Select>
<Label>State</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Normal', value: 'normal' },
{ label: 'Pending', value: 'pending' },
{ label: 'Firing', value: 'firing' },
]}
/>
<Label>Type</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Alert rule', value: 'alerting' },
{ label: 'Recording rule', value: 'recording' },
]}
/>
<Label>Health</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 variant="secondary">Clear</Button>
<Button>Apply</Button>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center">
<Label>Folder / Namespace</Label>
<Select {...register('namespace')} options={[]} onBlur={stopPropagation} />
<Label>Rule name</Label>
<Input {...register('name')} onBlur={stopPropagation} />
<Label>Evaluation group</Label>
<Input {...register('group')} onBlur={stopPropagation} />
<Label>Labels</Label>
<Input {...register('labels')} onBlur={stopPropagation} />
<Label>Data source</Label>
<MultipleDataSourcePicker
alerting
noDefault
placeholder="All data sources"
current={watch('dataSource')}
onChange={handleDataSourceChange}
onClear={() => resetField('dataSource')}
onBlur={stopPropagation}
/>
<Label>Dashboard</Label>
<Select {...register('dashboardUID')} options={[]} onBlur={stopPropagation} />
{pluginsFilterEnabled && (
<div>
<Label>From plugin</Label>
<RadioButtonGroup
options={PluginOptions}
{...register('plugins')}
value={watch('plugins')}
onChange={(value) => {
setValue('plugins', value);
}}
/>
</div>
)}
<Label>State</Label>
<RadioButtonGroup
options={RuleStateOptions}
{...register('state')}
value={watch('state')}
onChange={(value) => {
setValue('state', value);
}}
/>
<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>
</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