+
diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx
new file mode 100644
index 00000000000..7df3650276f
--- /dev/null
+++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from 'react';
+
+import { AlertManagerPicker } from '../AlertManagerPicker';
+import { MatcherFilter } from './MatcherFilter';
+import { AlertStateFilter } from './AlertStateFilter';
+import { GroupBy } from './GroupBy';
+import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
+import { GrafanaTheme2 } from '@grafana/data';
+import { Button, useStyles2 } from '@grafana/ui';
+
+import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
+import { css } from '@emotion/css';
+import { getFiltersFromUrlParams } from '../../utils/misc';
+import { useQueryParams } from 'app/core/hooks/useQueryParams';
+
+interface Props {
+ groups: AlertmanagerGroup[];
+}
+
+export const AlertGroupFilter = ({ groups }: Props) => {
+ const [filterKey, setFilterKey] = useState
(Math.floor(Math.random() * 100));
+ const [queryParams, setQueryParams] = useQueryParams();
+ const { groupBy = [], queryString, alertState } = getFiltersFromUrlParams(queryParams);
+ const matcherFilterKey = `matcher-${filterKey}`;
+
+ const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
+ const styles = useStyles2(getStyles);
+
+ const clearFilters = () => {
+ setQueryParams({
+ groupBy: null,
+ queryString: null,
+ alertState: null,
+ });
+ setTimeout(() => setFilterKey(filterKey + 1), 100);
+ };
+
+ const showClearButton = !!(groupBy.length > 0 || queryString || alertState);
+
+ return (
+
+
+
+
setQueryParams({ queryString: value ? value : null })}
+ />
+ setQueryParams({ groupBy: keys.length ? keys.join(',') : null })}
+ />
+ setQueryParams({ alertState: value ? value : null })}
+ />
+ {showClearButton && (
+
+ )}
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ wrapper: css`
+ border-bottom: 1px solid ${theme.colors.border.medium};
+ margin-bottom: ${theme.spacing(3)};
+ `,
+ filterSection: css`
+ display: flex;
+ flex-direction: row;
+ margin-bottom: ${theme.spacing(3)};
+ `,
+ filterInput: css`
+ width: 340px;
+ margin-left: ${theme.spacing(1)};
+ `,
+ clearButton: css`
+ margin-left: ${theme.spacing(1)};
+ margin-top: 19px;
+ `,
+});
diff --git a/public/app/features/alerting/unified/components/amnotifications/AmNotificationsGroupHeader.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupHeader.tsx
similarity index 94%
rename from public/app/features/alerting/unified/components/amnotifications/AmNotificationsGroupHeader.tsx
rename to public/app/features/alerting/unified/components/alert-groups/AlertGroupHeader.tsx
index 76464b2b9ac..9f6d9fd7cdb 100644
--- a/public/app/features/alerting/unified/components/amnotifications/AmNotificationsGroupHeader.tsx
+++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupHeader.tsx
@@ -8,7 +8,7 @@ interface Props {
group: AlertmanagerGroup;
}
-export const AmNotificationsGroupHeader = ({ group }: Props) => {
+export const AlertGroupHeader = ({ group }: Props) => {
const textStyles = useStyles2(getNotificationsTextColors);
const total = group.alerts.length;
const countByStatus = group.alerts.reduce((statusObj, alert) => {
diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertStateFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertStateFilter.tsx
new file mode 100644
index 00000000000..44cc945b443
--- /dev/null
+++ b/public/app/features/alerting/unified/components/alert-groups/AlertStateFilter.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { RadioButtonGroup, Label, useStyles2 } from '@grafana/ui';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
+import { AlertState } from 'app/plugins/datasource/alertmanager/types';
+import { css } from '@emotion/css';
+
+interface Props {
+ stateFilter?: AlertState;
+ onStateFilterChange: (value: AlertState) => void;
+}
+
+export const AlertStateFilter = ({ onStateFilterChange, stateFilter }: Props) => {
+ const styles = useStyles2(getStyles);
+ const alertStateOptions: SelectableValue[] = Object.entries(AlertState)
+ .sort(([labelA], [labelB]) => (labelA < labelB ? -1 : 1))
+ .map(([label, state]) => ({
+ label,
+ value: state,
+ }));
+
+ return (
+
+
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ wrapper: css`
+ margin-left: ${theme.spacing(1)};
+ `,
+});
diff --git a/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx b/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx
new file mode 100644
index 00000000000..ae595e84372
--- /dev/null
+++ b/public/app/features/alerting/unified/components/alert-groups/GroupBy.tsx
@@ -0,0 +1,37 @@
+import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
+import React from 'react';
+import { uniq } from 'lodash';
+import { Icon, Label, MultiSelect } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
+
+interface Props {
+ className?: string;
+ groups: AlertmanagerGroup[];
+ groupBy: string[];
+ onGroupingChange: (keys: string[]) => void;
+}
+
+export const GroupBy = ({ className, groups, groupBy, onGroupingChange }: Props) => {
+ const labelKeyOptions = uniq(groups.flatMap((group) => group.alerts).flatMap(({ labels }) => Object.keys(labels)))
+ .filter((label) => !(label.startsWith('__') && label.endsWith('__'))) // Filter out private labels
+ .map((key) => ({
+ label: key,
+ value: key,
+ }));
+
+ return (
+
+
+ }
+ onChange={(items) => {
+ onGroupingChange(items.map(({ value }) => value as string));
+ }}
+ options={labelKeyOptions}
+ />
+
+ );
+};
diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx
new file mode 100644
index 00000000000..e0652394d76
--- /dev/null
+++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx
@@ -0,0 +1,47 @@
+import React, { FormEvent } from 'react';
+import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { css } from '@emotion/css';
+
+interface Props {
+ className?: string;
+ queryString?: string;
+ onFilterChange: (filterString: string) => void;
+}
+
+export const MatcherFilter = ({ className, onFilterChange, queryString }: Props) => {
+ const styles = useStyles2(getStyles);
+ const handleSearchChange = (e: FormEvent) => {
+ const target = e.target as HTMLInputElement;
+ onFilterChange(target.value);
+ };
+ return (
+
+
+ }
+ >
+
+
+ Search by label
+
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ icon: css`
+ margin-right: ${theme.spacing(0.5)};
+ `,
+});
diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
index ca6f4a91603..403733980c2 100644
--- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
@@ -61,7 +61,7 @@ const RulesFilter = () => {
queryString: null,
dataSource: null,
});
- setFilterKey(filterKey + 1);
+ setTimeout(() => setFilterKey(filterKey + 1), 100);
};
const searchIcon =