Alerting: New search UI – Part 1 (#91620)

pull/93274/head
Gilles De Mey 10 months ago committed by GitHub
parent 05023d9d31
commit 90ee52e8d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 32
      .betterer.results
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 6
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles_gen.csv
  5. 4
      pkg/services/featuremgmt/toggles_gen.go
  6. 13
      pkg/services/featuremgmt/toggles_gen.json
  7. 42
      public/app/features/alerting/unified/components/HoverCard.tsx
  8. 6
      public/app/features/alerting/unified/components/Tokenize.tsx
  9. 6
      public/app/features/alerting/unified/components/expressions/Expression.tsx
  10. 6
      public/app/features/alerting/unified/components/notification-policies/Matchers.tsx
  11. 14
      public/app/features/alerting/unified/components/notification-policies/Policy.tsx
  12. 6
      public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx
  13. 10
      public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx
  14. 62
      public/app/features/alerting/unified/components/rule-list/RuleList.v1.tsx
  15. 4
      public/app/features/alerting/unified/components/rule-list/RuleList.v2.tsx
  16. 18
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx
  17. 31
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx
  18. 222
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx
  19. 2
      public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
  20. 6
      public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx
  21. 19
      public/locales/en-US/grafana.json
  22. 19
      public/locales/pseudo-LOCALE/grafana.json

@ -2261,6 +2261,22 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
],
"public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"]
],
"public/app/features/alerting/unified/components/rules/GrafanaRules.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
@ -2313,22 +2329,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/RulesFilter.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"]
],
"public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],

@ -201,6 +201,7 @@ export interface FeatureToggles {
bodyScrolling?: boolean;
cloudwatchMetricInsightsCrossAccount?: boolean;
prometheusAzureOverrideAudience?: boolean;
alertingFilterV2?: boolean;
backgroundPluginInstaller?: boolean;
dataplaneAggregator?: boolean;
newFiltersUI?: boolean;

@ -1385,6 +1385,12 @@ var (
Stage: FeatureStageDeprecated,
Owner: grafanaPartnerPluginsSquad,
Expression: "true", // Enabled by default for now
}, {
Name: "alertingFilterV2",
Description: "Enable the new alerting search experience",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromDocs: true,
},
{
Name: "backgroundPluginInstaller",

@ -182,6 +182,7 @@ cloudWatchRoundUpEndTime,GA,@grafana/aws-datasources,false,false,false
bodyScrolling,preview,@grafana/grafana-frontend-platform,false,false,true
cloudwatchMetricInsightsCrossAccount,preview,@grafana/aws-datasources,false,false,true
prometheusAzureOverrideAudience,deprecated,@grafana/partner-datasources,false,false,false
alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false
backgroundPluginInstaller,experimental,@grafana/plugins-platform-backend,false,true,false
dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
newFiltersUI,experimental,@grafana/dashboards-squad,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
182 bodyScrolling preview @grafana/grafana-frontend-platform false false true
183 cloudwatchMetricInsightsCrossAccount preview @grafana/aws-datasources false false true
184 prometheusAzureOverrideAudience deprecated @grafana/partner-datasources false false false
185 alertingFilterV2 experimental @grafana/alerting-squad false false false
186 backgroundPluginInstaller experimental @grafana/plugins-platform-backend false true false
187 dataplaneAggregator experimental @grafana/grafana-app-platform-squad false true false
188 newFiltersUI experimental @grafana/dashboards-squad false false false

@ -739,6 +739,10 @@ const (
// Deprecated. Allow override default AAD audience for Azure Prometheus endpoint. Enabled by default. This feature should no longer be used and will be removed in the future.
FlagPrometheusAzureOverrideAudience = "prometheusAzureOverrideAudience"
// FlagAlertingFilterV2
// Enable the new alerting search experience
FlagAlertingFilterV2 = "alertingFilterV2"
// FlagBackgroundPluginInstaller
// Enable background plugin installer
FlagBackgroundPluginInstaller = "backgroundPluginInstaller"

@ -162,6 +162,19 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingFilterV2",
"resourceVersion": "1723028774805",
"creationTimestamp": "2024-08-07T11:06:14Z"
},
"spec": {
"description": "Enable the new alerting search experience",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingInsights",

@ -6,7 +6,7 @@ import { cloneElement, ReactElement, ReactNode, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Popover as GrafanaPopover, PopoverController, useStyles2, Stack } from '@grafana/ui';
export interface HoverCardProps {
export interface PopupCardProps {
children: ReactElement;
header?: ReactNode;
content: ReactElement;
@ -16,9 +16,10 @@ export interface HoverCardProps {
disabled?: boolean;
showAfter?: number;
arrow?: boolean;
showOn?: 'click' | 'hover';
}
export const HoverCard = ({
export const PopupCard = ({
children,
header,
content,
@ -27,8 +28,9 @@ export const HoverCard = ({
showAfter = 300,
wrapperClassName,
disabled = false,
showOn = 'hover',
...rest
}: HoverCardProps) => {
}: PopupCardProps) => {
const popoverRef = useRef<HTMLElement>(null);
const styles = useStyles2(getStyles);
@ -36,6 +38,9 @@ export const HoverCard = ({
return children;
}
const showOnHover = showOn === 'hover';
const showOnClick = showOn === 'click';
const body = (
<Stack direction="column" gap={0} role="tooltip">
{header && <div className={styles.card.header}>{header}</div>}
@ -47,6 +52,21 @@ export const HoverCard = ({
return (
<PopoverController content={body} hideAfter={100}>
{(showPopper, hidePopper, popperProps) => {
// support hover and click interaction
const onClickProps = {
onClick: showPopper,
};
const onHoverProps = {
onMouseLeave: hidePopper,
onMouseEnter: showPopper,
};
const blurFocusProps = {
onBlur: hidePopper,
onFocus: showPopper,
};
return (
<>
{popoverRef.current && (
@ -54,22 +74,24 @@ export const HoverCard = ({
{...popperProps}
{...rest}
wrapperClassName={classnames(styles.popover, wrapperClassName)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
onFocus={showPopper}
onBlur={hidePopper}
referenceElement={popoverRef.current}
renderArrow={arrow}
// @TODO
// if we want interaction with the content we should not pass blur / focus handlers but then clicking outside doesn't close the popper
{...blurFocusProps}
// if we want hover interaction we have to make sure we add the leave / enter handlers
{...(showOnHover ? onHoverProps : {})}
/>
)}
{cloneElement(children, {
ref: popoverRef,
onMouseEnter: showPopper,
onMouseLeave: hidePopper,
onFocus: showPopper,
onBlur: hidePopper,
tabIndex: 0,
// make sure we pass the correct interaction handlers here to the element we want to interact with
...(showOnHover ? onHoverProps : {}),
...(showOnClick ? onClickProps : {}),
})}
</>
);
@ -83,7 +105,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.medium}`,
border: `1px solid ${theme.colors.border.weak}`,
}),
card: {
body: css({

@ -4,7 +4,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Badge, useStyles2 } from '@grafana/ui';
import { HoverCard } from './HoverCard';
import { PopupCard } from './HoverCard';
import { keywords as KEYWORDS, builtinFunctions as FUNCTIONS } from './receivers/editor/language';
const VARIABLES = ['$', '.', '"'];
@ -83,7 +83,7 @@ function Token({ content, description, type }: TokenProps) {
const disableCard = Boolean(type) === false;
return (
<HoverCard
<PopupCard
placement="top-start"
disabled={disableCard}
content={
@ -95,7 +95,7 @@ function Token({ content, description, type }: TokenProps) {
<span>
<Badge tabIndex={0} className={styles.token} text={content} color={'blue'} />
</span>
</HoverCard>
</PopupCard>
);
}

@ -20,7 +20,7 @@ import {
import { AlertQuery, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { usePagination } from '../../hooks/usePagination';
import { HoverCard } from '../HoverCard';
import { PopupCard } from '../HoverCard';
import { Spacer } from '../Spacer';
import { AlertStateTag } from '../rules/AlertStateTag';
@ -424,7 +424,7 @@ const TimeseriesRow: FC<FrameProps & { index: number }> = ({ frame, index }) =>
{name}
</span>
<div className={styles.expression.resultValue}>
<HoverCard
<PopupCard
placement="right"
wrapperClassName={styles.timeseriesTableWrapper}
content={
@ -447,7 +447,7 @@ const TimeseriesRow: FC<FrameProps & { index: number }> = ({ frame, index }) =>
}
>
<span>Time series data</span>
</HoverCard>
</PopupCard>
</div>
</Stack>
</div>

@ -7,7 +7,7 @@ import { getTagColorsFromName, useStyles2, Stack } from '@grafana/ui';
import { ObjectMatcher } from 'app/plugins/datasource/alertmanager/types';
import { MatcherFormatter, matcherFormatter } from '../../utils/matchers';
import { HoverCard } from '../HoverCard';
import { PopupCard } from '../HoverCard';
type MatchersProps = { matchers: ObjectMatcher[]; formatter?: MatcherFormatter };
@ -29,7 +29,7 @@ const Matchers: FC<MatchersProps> = ({ matchers, formatter = 'default' }) => {
))}
{/* TODO hover state to show all matchers we're not showing */}
{hasMoreMatchers && (
<HoverCard
<PopupCard
arrow
placement="top"
content={
@ -43,7 +43,7 @@ const Matchers: FC<MatchersProps> = ({ matchers, formatter = 'default' }) => {
<span>
<div className={styles.metadata}>{`and ${rest.length} more`}</div>
</span>
</HoverCard>
</PopupCard>
)}
</Stack>
</span>

@ -42,7 +42,7 @@ import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
import { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies';
import { InsertPosition } from '../../utils/routeTree';
import { Authorize } from '../Authorize';
import { HoverCard } from '../HoverCard';
import { PopupCard } from '../HoverCard';
import { Label } from '../Label';
import { MetaText } from '../MetaText';
import { ProvisioningBadge } from '../Provisioning';
@ -633,7 +633,7 @@ const ProvisionedTooltip = (children: ReactNode) => (
);
const Errors: FC<{ errors: React.ReactNode[] }> = ({ errors }) => (
<HoverCard
<PopupCard
arrow
placement="top"
content={
@ -647,7 +647,7 @@ const Errors: FC<{ errors: React.ReactNode[] }> = ({ errors }) => (
<span>
<Badge icon="exclamation-circle" color="red" text={pluralize('error', errors.length, true)} />
</span>
</HoverCard>
</PopupCard>
);
const ContinueMatchingIndicator: FC = () => {
@ -697,7 +697,7 @@ function AutogeneratedRootIndicator() {
}
const InheritedProperties: FC<{ properties: InheritableProperties }> = ({ properties }) => (
<HoverCard
<PopupCard
arrow
placement="top"
content={
@ -715,7 +715,7 @@ const InheritedProperties: FC<{ properties: InheritableProperties }> = ({ proper
<div>
<Text color="primary">{pluralize('property', Object.keys(properties).length, true)}</Text>
</div>
</HoverCard>
</PopupCard>
);
const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> = ({
@ -884,7 +884,7 @@ const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
const groupedIntegrations = groupBy(details.grafana_managed_receiver_configs, (config) => config.type);
return (
<HoverCard
<PopupCard
arrow
placement="top"
header={
@ -918,7 +918,7 @@ const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
>
{contactPoint}
</TextLink>
</HoverCard>
</PopupCard>
);
};

@ -2,7 +2,7 @@ import { forwardRef } from 'react';
import { Icon, Input } from '@grafana/ui';
import { HoverCard } from '../HoverCard';
import { PopupCard } from '../HoverCard';
import { PromDurationDocs } from './PromDurationDocs';
@ -10,9 +10,9 @@ export const PromDurationInput = forwardRef<HTMLInputElement, React.ComponentPro
return (
<Input
suffix={
<HoverCard content={<PromDurationDocs />} disabled={false}>
<PopupCard content={<PromDurationDocs />} disabled={false}>
<Icon name="info-circle" size="lg" />
</HoverCard>
</PopupCard>
}
{...props}
ref={ref}

@ -4,7 +4,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Stack } from '@grafana/ui';
import { HoverCard } from '../HoverCard';
import { PopupCard } from '../HoverCard';
import {
AlertTemplateData,
@ -35,13 +35,13 @@ export function TemplateDataDocs() {
dataItems={GlobalTemplateData}
typeRenderer={(type) =>
type === '[]Alert' ? (
<HoverCard content={AlertTemplateDataTable}>
<PopupCard content={AlertTemplateDataTable}>
<div className={styles.interactiveType}>{type}</div>
</HoverCard>
</PopupCard>
) : type === 'KeyValue' ? (
<HoverCard content={<KeyValueTemplateDataTable />}>
<PopupCard content={<KeyValueTemplateDataTable />}>
<div className={styles.interactiveType}>{type}</div>
</HoverCard>
</PopupCard>
) : (
type
)

@ -1,10 +1,9 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom-v5-compat';
import { useAsyncFn, useInterval } from 'react-use';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
import { urlUtil } from '@grafana/data';
import { Button, LinkButton, Stack, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types';
@ -18,13 +17,13 @@ import { fetchAllPromAndRulerRulesAction } from '../../state/actions';
import { RULE_LIST_POLL_INTERVAL_MS } from '../../utils/constants';
import { getAllRulesSourceNames } from '../../utils/datasource';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
import RulesFilter from '../rules/Filter/RulesFilter';
import { NoRulesSplash } from '../rules/NoRulesCTA';
import { INSTANCES_DISPLAY_LIMIT } from '../rules/RuleDetails';
import { RuleListErrors } from '../rules/RuleListErrors';
import { RuleListGroupView } from '../rules/RuleListGroupView';
import { RuleListStateView } from '../rules/RuleListStateView';
import { RuleStats } from '../rules/RuleStats';
import RulesFilter from '../rules/RulesFilter';
const VIEWS = {
groups: RuleListGroupView,
@ -37,7 +36,6 @@ const LIMIT_ALERTS = INSTANCES_DISPLAY_LIMIT + 1;
const RuleList = withErrorBoundary(
() => {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
const [expandAll, setExpandAll] = useState(false);
@ -106,26 +104,20 @@ const RuleList = withErrorBoundary(
// We show separate indicators for Grafana-managed and Cloud rules
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={hasAlertRulesCreated && <CreateAlertButton />}>
<RuleListErrors />
<RulesFilter onFilterCleared={onFilterCleared} />
<RulesFilter onClear={onFilterCleared} />
{hasAlertRulesCreated && (
<>
<div className={styles.break} />
<div className={styles.buttonsContainer}>
<div className={styles.statsContainer}>
{view === 'groups' && hasActiveFilters && (
<Button
className={styles.expandAllButton}
icon={expandAll ? 'angle-double-up' : 'angle-double-down'}
variant="secondary"
onClick={() => setExpandAll(!expandAll)}
>
{expandAll ? 'Collapse all' : 'Expand all'}
</Button>
)}
<RuleStats namespaces={filteredNamespaces} />
</div>
</div>
</>
<Stack direction="row" alignItems="center">
{view === 'groups' && hasActiveFilters && (
<Button
icon={expandAll ? 'angle-double-up' : 'angle-double-down'}
variant="secondary"
onClick={() => setExpandAll(!expandAll)}
>
{expandAll ? 'Collapse all' : 'Expand all'}
</Button>
)}
<RuleStats namespaces={filteredNamespaces} />
</Stack>
)}
{hasNoAlertRulesCreatedYet && <NoRulesSplash />}
{hasAlertRulesCreated && <ViewComponent expandAll={expandAll} namespaces={filteredNamespaces} />}
@ -135,28 +127,6 @@ const RuleList = withErrorBoundary(
{ style: 'page' }
);
const getStyles = (theme: GrafanaTheme2) => ({
break: css({
width: '100%',
height: 0,
marginBottom: theme.spacing(2),
borderBottom: `solid 1px ${theme.colors.border.medium}`,
}),
buttonsContainer: css({
marginBottom: theme.spacing(2),
display: 'flex',
justifyContent: 'space-between',
}),
statsContainer: css({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}),
expandAllButton: css({
marginRight: theme.spacing(1),
}),
});
export default RuleList;
export function CreateAlertButton() {

@ -18,11 +18,11 @@ import { RULE_LIST_POLL_INTERVAL_MS } from '../../utils/constants';
import { getAllRulesSourceNames, getRulesSourceUniqueKey, getApplicationFromRulesSource } from '../../utils/datasource';
import { makeFolderAlertsLink } from '../../utils/misc';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
import RulesFilter from '../rules/Filter/RulesFilter';
import { NoRulesSplash } from '../rules/NoRulesCTA';
import { INSTANCES_DISPLAY_LIMIT } from '../rules/RuleDetails';
import { RuleListErrors } from '../rules/RuleListErrors';
import { RuleStats } from '../rules/RuleStats';
import RulesFilter from '../rules/RulesFilter';
import { EvaluationGroupWithRules } from './EvaluationGroupWithRules';
import Namespace from './Namespace';
@ -101,7 +101,7 @@ const RuleList = withErrorBoundary(
// We show separate indicators for Grafana-managed and Cloud rules
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={hasAlertRulesCreated && <CreateAlertButton />}>
<RuleListErrors />
<RulesFilter onFilterCleared={onFilterCleared} />
<RulesFilter onClear={onFilterCleared} />
{hasAlertRulesCreated && (
<>
<div className={styles.break} />

@ -0,0 +1,18 @@
import { Suspense, lazy } from 'react';
import { config } from '@grafana/runtime';
import RulesFilterV1 from './RulesFilter.v1';
const RulesFilterV2 = lazy(() => import('./RulesFilter.v2'));
interface RulesFilerProps {
onClear?: () => void;
}
const RulesFilter = (props: RulesFilerProps) => {
const newView = config.featureToggles.alertingFilterV2;
return <Suspense>{newView ? <RulesFilterV2 {...props} /> : <RulesFilterV1 {...props} />}</Suspense>;
};
export default RulesFilter;

@ -16,17 +16,16 @@ import {
trackRulesListViewChange,
trackRulesSearchComponentInteraction,
trackRulesSearchInputInteraction,
} from '../../Analytics';
import { useRulesFilter } from '../../hooks/useFilteredRules';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { useAlertingHomePageExtensions } from '../../plugins/useAlertingHomePageExtensions';
import { RuleHealth } from '../../search/rulesSearchParser';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { alertStateToReadable } from '../../utils/rules';
import { HoverCard } from '../HoverCard';
import { MultipleDataSourcePicker } from './MultipleDataSourcePicker';
} from '../../../Analytics';
import { useRulesFilter } from '../../../hooks/useFilteredRules';
import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
import { useAlertingHomePageExtensions } from '../../../plugins/useAlertingHomePageExtensions';
import { RuleHealth } from '../../../search/rulesSearchParser';
import { AlertmanagerProvider } from '../../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { alertStateToReadable } from '../../../utils/rules';
import { PopupCard } from '../../HoverCard';
import { MultipleDataSourcePicker } from '../MultipleDataSourcePicker';
const ViewOptions: SelectableValue[] = [
{
@ -64,7 +63,7 @@ const RuleHealthOptions: SelectableValue[] = [
];
interface RulesFilerProps {
onFilterCleared?: () => void;
onClear?: () => void;
}
const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({
@ -72,7 +71,7 @@ const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]
value,
}));
const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => {
const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
const styles = useStyles2(getStyles);
const [queryParams, updateQueryParams] = useURLSearchParams();
const { pluginsFilterEnabled } = usePluginsFilterStatus();
@ -136,7 +135,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
const handleClearFiltersClick = () => {
setSearchQuery(undefined);
onFilterCleared();
onClear();
setTimeout(() => setFilterKey(filterKey + 1), 100);
};
@ -291,9 +290,9 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
<Label htmlFor="rulesSearchInput">
<Stack gap={0.5} alignItems="center">
<span>Search</span>
<HoverCard content={<SearchQueryHelp />}>
<PopupCard content={<SearchQueryHelp />}>
<Icon name="info-circle" size="sm" tabIndex={0} title="Search help" />
</HoverCard>
</PopupCard>
</Stack>
</Label>
}

@ -0,0 +1,222 @@
import { css } from '@emotion/css';
import { useCallback, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import {
Badge,
Button,
Grid,
IconButton,
Input,
InteractiveTable,
Label,
RadioButtonGroup,
Select,
Stack,
Tab,
TabsBar,
useStyles2,
} from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { PopupCard } from '../../HoverCard';
import MoreButton from '../../MoreButton';
type RulesFilterProps = {
onClear?: () => void;
};
type ActiveTab = 'custom' | 'saved';
export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
const styles = useStyles2(getStyles);
const [activeTab, setActiveTab] = useState<ActiveTab>('custom');
const filterOptions = useMemo(() => {
return (
<PopupCard
showOn="click"
placement="bottom-start"
content={
<div className={styles.content}>
{activeTab === 'custom' && <FilterOptions />}
{activeTab === 'saved' && <SavedSearches />}
</div>
}
header={
<TabsBar hideBorder className={styles.fixTabsMargin}>
<Tab
active={activeTab === 'custom'}
icon="filter"
label={'Custom filter'}
onChangeTab={() => setActiveTab('custom')}
/>
<Tab
active={activeTab === 'saved'}
icon="bookmark"
label={'Saved searches'}
onChangeTab={() => setActiveTab('saved')}
/>
</TabsBar>
}
>
<IconButton name="filter" aria-label="Show filters" />
</PopupCard>
);
}, [activeTab, styles.content, styles.fixTabsMargin]);
return (
<Stack direction="column" gap={0}>
<Label>
<Trans i18nKey="common.search">Search</Trans>
</Label>
<Stack direction="row">
<Input prefix={filterOptions} />
</Stack>
</Stack>
);
}
const FilterOptions = () => {
return (
<Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center">
<Label>
<Trans i18nKey="alerting.search.property.namespace">Folder / Namespace</Trans>
</Label>
<Select options={[]} onChange={() => {}}></Select>
<Label>
<Trans i18nKey="alerting.search.property.rule-name">Alerting rule name</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.evaluation-group">Evaluation group</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.labels">Labels</Trans>
</Label>
<Input />
<Label>
<Trans i18nKey="alerting.search.property.data-source">Data source</Trans>
</Label>
<Select options={[]} onChange={() => {}}></Select>
<Label>
<Trans i18nKey="alerting.search.property.state">State</Trans>
</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Normal', value: 'normal' },
{ label: 'Pending', value: 'pending' },
{ label: 'Firing', value: 'firing' },
]}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-type">Type</Trans>
</Label>
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Alert rule', value: 'alerting' },
{ label: 'Recording rule', value: 'recording' },
]}
/>
<Label>
<Trans i18nKey="alerting.search.property.rule-health">Health</Trans>
</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">
<Trans i18nKey="common.clear">Clear</Trans>
</Button>
<Button>
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
</Stack>
</Stack>
);
};
type TableColumns = {
name: string;
default?: boolean;
};
const SavedSearches = () => {
const applySearch = useCallback((name: string) => {}, []);
return (
<>
<Stack direction="column" gap={2} alignItems="flex-end">
<Button variant="secondary" size="sm">
<Trans i18nKey="alerting.search.save-query">Save current search</Trans>
</Button>
<InteractiveTable<TableColumns>
columns={[
{
id: 'name',
header: 'Saved search name',
cell: ({ row }) => (
<Stack alignItems="center">
{row.original.name}
{row.original.default ? <Badge text="Default" color="blue" /> : null}
</Stack>
),
},
{
id: 'actions',
cell: ({ row }) => (
<Stack direction="row" alignItems="center">
<Button variant="secondary" fill="outline" size="sm" onClick={() => applySearch(row.original.name)}>
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
<MoreButton size="sm" fill="outline" />
</Stack>
),
},
]}
data={[
{
name: 'My saved search',
default: true,
},
{
name: 'Another saved search',
},
{
name: 'This one has a really long name and some emojis too 🥒',
},
]}
getRowId={(row) => row.name}
/>
<Button variant="secondary">
<Trans i18nKey="common.close">Close</Trans>
</Button>
</Stack>
</>
);
};
function getStyles(theme: GrafanaTheme2) {
return {
content: css({
padding: theme.spacing(1),
maxWidth: 500,
}),
fixTabsMargin: css({
marginTop: theme.spacing(-1),
}),
};
}

@ -8,7 +8,7 @@ import * as analytics from '../../Analytics';
import { MockDataSourceSrv } from '../../mocks';
import { setupPluginsExtensionsHook } from '../../testSetup/plugins';
import RulesFilter from './RulesFilter';
import RulesFilter from './Filter/RulesFilter';
setupMswServer();
jest.spyOn(analytics, 'logInfo');

@ -10,7 +10,7 @@ import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 }
import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { combineMatcherStrings } from '../../../utils/alertmanager';
import { AlertLabels } from '../../AlertLabels';
import { HoverCard } from '../../HoverCard';
import { PopupCard } from '../../HoverCard';
import { LogRecordViewerByTimestamp } from './LogRecordViewer';
import { LogTimelineViewer } from './LogTimelineViewer';
@ -186,7 +186,7 @@ const SearchFieldInput = React.forwardRef<HTMLInputElement, SearchFieldInputProp
<Label htmlFor="instancesSearchInput">
<Stack gap={0.5}>
<span>Filter instances</span>
<HoverCard
<PopupCard
content={
<>
Use label matcher expression (like <code>{'{foo=bar}'}</code>) or click on an instance label to
@ -195,7 +195,7 @@ const SearchFieldInput = React.forwardRef<HTMLInputElement, SearchFieldInputProp
}
>
<Icon name="info-circle" size="sm" />
</HoverCard>
</PopupCard>
</Stack>
</Label>
}

@ -225,6 +225,19 @@
"update-rule": {
"success": "Rule updated successfully"
}
},
"search": {
"property": {
"data-source": "Data source",
"evaluation-group": "Evaluation group",
"labels": "Labels",
"namespace": "Folder / Namespace",
"rule-health": "Health",
"rule-name": "Alerting rule name",
"rule-type": "Type",
"state": "State"
},
"save-query": "Save current search"
}
},
"annotations": {
@ -367,11 +380,15 @@
}
},
"common": {
"apply": "Apply",
"cancel": "Cancel",
"clear": "Clear",
"close": "Close",
"locale": {
"default": "Default"
},
"save": "Save"
"save": "Save",
"search": "Search"
},
"configuration-tracker": {
"config-card": {

@ -225,6 +225,19 @@
"update-rule": {
"success": "Ŗūľę ūpđäŧęđ şūččęşşƒūľľy"
}
},
"search": {
"property": {
"data-source": "Đäŧä şőūřčę",
"evaluation-group": "Ēväľūäŧįőʼn ģřőūp",
"labels": "Ŀäþęľş",
"namespace": "Főľđęř / Ńämęşpäčę",
"rule-health": "Ħęäľŧĥ",
"rule-name": "Åľęřŧįʼnģ řūľę ʼnämę",
"rule-type": "Ŧypę",
"state": "Ŝŧäŧę"
},
"save-query": "Ŝävę čūřřęʼnŧ şęäřčĥ"
}
},
"annotations": {
@ -367,11 +380,15 @@
}
},
"common": {
"apply": "Åppľy",
"cancel": "Cäʼnčęľ",
"clear": "Cľęäř",
"close": "Cľőşę",
"locale": {
"default": "Đęƒäūľŧ"
},
"save": "Ŝävę"
"save": "Ŝävę",
"search": "Ŝęäřčĥ"
},
"configuration-tracker": {
"config-card": {

Loading…
Cancel
Save