From 732ba19d05e690779b4373f1ee5b565f1d25a785 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Tue, 13 Aug 2024 13:13:07 +0200 Subject: [PATCH] WIP --- .../grafana-ui/src/components/Select/types.ts | 4 +- .../components/rules/Filter/Options.tsx | 59 +++++ .../rules/Filter/RulesFilter.v1.tsx | 2 +- .../rules/Filter/RulesFilter.v2.tsx | 224 +++++++++++++----- 4 files changed, 233 insertions(+), 56 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/Filter/Options.tsx diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index ae97e63adfd..445b48a694e 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -65,7 +65,7 @@ export interface SelectCommonProps { menuShouldPortal?: boolean; /** The message to display when no options could be found */ noOptionsMessage?: string; - onBlur?: () => void; + onBlur?: (e: React.FocusEvent) => void; onChange: (value: SelectableValue, 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 { /** 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) => void; openMenuOnFocus?: boolean; options?: Array>; placeholder?: string; diff --git a/public/app/features/alerting/unified/components/rules/Filter/Options.tsx b/public/app/features/alerting/unified/components/rules/Filter/Options.tsx new file mode 100644 index 00000000000..b093e3c6f35 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/Filter/Options.tsx @@ -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 | '*'; + +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>> = [ + { + label: 'All', + value: '*', + }, + { + label: 'Alert rule', + value: PromRuleType.Alerting, + }, + { + label: 'Recording rule', + value: PromRuleType.Recording, + }, +]; + +export const RuleHealthOptions: Array>> = [ + { label: 'All', value: '*' }, + { label: 'Ok', value: RuleHealth.Ok }, + { label: 'No Data', value: RuleHealth.NoData }, + { label: 'Error', value: RuleHealth.Error }, +]; + +export const RuleStateOptions: Array>> = [ + { label: 'All', value: '*' }, + { label: 'Normal', value: PromAlertingRuleState.Inactive }, + { label: 'Pending', value: PromAlertingRuleState.Pending }, + { label: 'Firing', value: PromAlertingRuleState.Firing }, +]; + +export const PluginOptions: Array> = [ + { label: 'Show', value: undefined }, + { label: 'Hide', value: 'hide' }, +]; diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index f2d6190cfcb..52a65f867f3 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -362,7 +362,7 @@ const helpStyles = (theme: GrafanaTheme2) => ({ }), }); -function usePluginsFilterStatus() { +export function usePluginsFilterStatus() { const { extensions } = useAlertingHomePageExtensions(); return { pluginsFilterEnabled: extensions.length > 0 }; } diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx index 4339d5e26a2..f4206bf0de7 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx @@ -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; + type: WithAnyOption<'alerting' | 'recording'>; + health: WithAnyOption; + 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('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 ( - - - - +
+ + + + + + + + + updateQueryParams({ view })} + /> + - +
); } const FilterOptions = () => { + const { pluginsFilterEnabled } = usePluginsFilterStatus(); + const { updateFilters, filterState } = useRulesFilter(); + const { register, handleSubmit, setValue, reset, resetField, watch, getValues } = useForm({ + 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 ( - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + resetField('dataSource')} + onBlur={stopPropagation} + /> + +