mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: New search UI – Part 1 (#91620)
This commit is contained in:
parent
05023d9d31
commit
90ee52e8d9
@ -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
|
||||
|
|
@ -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…
Reference in New Issue
Block a user