Alerting: List V2 - Improve error handling (#106282)

* Add error badge to the DataSourceSection component

* Fix lint

* Update translations

* Fix error propagation from featureDiscoveryApi

* Pass errors to GMA loader
titolins/update-alerting-prom-am
Konrad Lalik 3 weeks ago committed by GitHub
parent 1837f32d76
commit 7d5a26a4e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 36
      public/app/features/alerting/unified/api/featureDiscoveryApi.ts
  2. 6
      public/app/features/alerting/unified/rule-list/GroupedView.tsx
  3. 4
      public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx
  4. 10
      public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx
  5. 19
      public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx
  6. 1
      public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx
  7. 4
      public/locales/en-US/grafana.json

@ -59,24 +59,28 @@ export const featureDiscoveryApi = alertingApi.injectEndpoints({
return { error: new Error(`Missing data source configuration for ${rulesSourceIdentifier}`) }; return { error: new Error(`Missing data source configuration for ${rulesSourceIdentifier}`) };
} }
const features = await discoverFeaturesByUid(dataSourceSettings.uid); try {
const features = await discoverFeaturesByUid(dataSourceSettings.uid);
const rulerConfig = features.features.rulerApiEnabled const rulerConfig = features.features.rulerApiEnabled
? ({ ? ({
dataSourceName: dataSourceSettings.name, dataSourceName: dataSourceSettings.name,
dataSourceUid: dataSourceSettings.uid, dataSourceUid: dataSourceSettings.uid,
apiVersion: features.application === PromApplication.Cortex ? 'legacy' : 'config', apiVersion: features.application === PromApplication.Cortex ? 'legacy' : 'config',
} satisfies RulerDataSourceConfig) } satisfies RulerDataSourceConfig)
: undefined; : undefined;
return { return {
data: { data: {
name: dataSourceSettings.name, name: dataSourceSettings.name,
uid: dataSourceSettings.uid, uid: dataSourceSettings.uid,
application: features.application, application: features.application,
rulerConfig, rulerConfig,
} satisfies RulesSourceFeatures, } satisfies RulesSourceFeatures,
}; };
} catch (error) {
return { error: error };
}
}, },
}), }),
}), }),

@ -38,7 +38,7 @@ export function GrafanaDataSourceLoader() {
} }
function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) { function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) {
const { data: dataSourceInfo, isLoading } = useDiscoverDsFeaturesQuery({ uid: rulesSourceIdentifier.uid }); const { data: dataSourceInfo, isLoading, error } = useDiscoverDsFeaturesQuery({ uid: rulesSourceIdentifier.uid });
const { uid, name } = rulesSourceIdentifier; const { uid, name } = rulesSourceIdentifier;
@ -46,6 +46,10 @@ function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) {
return <DataSourceSection loader={<Skeleton width={250} height={16} />} uid={uid} name={name} />; return <DataSourceSection loader={<Skeleton width={250} height={16} />} uid={uid} name={name} />;
} }
if (error) {
return <DataSourceSection error={error} uid={uid} name={name} />;
}
// 2. grab prometheus rule groups with max_groups if supported // 2. grab prometheus rule groups with max_groups if supported
if (dataSourceInfo) { if (dataSourceInfo) {
return ( return (

@ -41,7 +41,7 @@ export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application }
}; };
}, [groupsGenerator]); }, [groupsGenerator]);
const { isLoading, groups, hasMoreGroups, fetchMoreGroups } = useLazyLoadPrometheusGroups( const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups(
groupsGenerator.current, groupsGenerator.current,
DATA_SOURCE_GROUP_PAGE_SIZE DATA_SOURCE_GROUP_PAGE_SIZE
); );
@ -50,7 +50,7 @@ export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application }
const groupsByNamespace = useMemo(() => groupBy(groups, 'file'), [groups]); const groupsByNamespace = useMemo(() => groupBy(groups, 'file'), [groups]);
return ( return (
<DataSourceSection name={name} application={application} uid={uid} isLoading={isLoading}> <DataSourceSection name={name} application={application} uid={uid} isLoading={isLoading} error={error}>
<Stack direction="column" gap={0}> <Stack direction="column" gap={0}>
{Object.entries(groupsByNamespace).map(([namespace, groups]) => ( {Object.entries(groupsByNamespace).map(([namespace, groups]) => (
<ListSection <ListSection

@ -33,7 +33,7 @@ export function PaginatedGrafanaLoader() {
}; };
}, []); }, []);
const { isLoading, groups, hasMoreGroups, fetchMoreGroups } = useLazyLoadPrometheusGroups( const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups(
groupsGenerator.current, groupsGenerator.current,
GRAFANA_GROUP_PAGE_SIZE GRAFANA_GROUP_PAGE_SIZE
); );
@ -42,7 +42,13 @@ export function PaginatedGrafanaLoader() {
const hasNoRules = isEmpty(groups) && !isLoading; const hasNoRules = isEmpty(groups) && !isLoading;
return ( return (
<DataSourceSection name="Grafana" application="grafana" uid={GrafanaRulesSourceSymbol} isLoading={isLoading}> <DataSourceSection
name="Grafana"
application="grafana"
uid={GrafanaRulesSourceSymbol}
isLoading={isLoading}
error={error}
>
<Stack direction="column" gap={0}> <Stack direction="column" gap={0}>
{Object.entries(groupsByFolder).map(([folderUid, groups]) => { {Object.entries(groupsByFolder).map(([folderUid, groups]) => {
// Groups are grouped by folder, so we can use the first group to get the folder name // Groups are grouped by folder, so we can use the first group to get the folder name

@ -4,13 +4,13 @@ import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { IconButton, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { Button, IconButton, LinkButton, Stack, Text, Toggletip, useStyles2 } from '@grafana/ui';
import { GrafanaRulesSourceSymbol, RulesSourceIdentifier } from 'app/types/unified-alerting'; import { GrafanaRulesSourceSymbol, RulesSourceIdentifier } from 'app/types/unified-alerting';
import { RulesSourceApplication } from 'app/types/unified-alerting-dto'; import { RulesSourceApplication } from 'app/types/unified-alerting-dto';
import { Spacer } from '../../components/Spacer'; import { Spacer } from '../../components/Spacer';
import { WithReturnButton } from '../../components/WithReturnButton'; import { WithReturnButton } from '../../components/WithReturnButton';
import { isAdmin } from '../../utils/misc'; import { isAdmin, stringifyErrorLike } from '../../utils/misc';
import { DataSourceIcon } from './Namespace'; import { DataSourceIcon } from './Namespace';
import { LoadingIndicator } from './RuleGroup'; import { LoadingIndicator } from './RuleGroup';
@ -22,6 +22,7 @@ export interface DataSourceSectionProps extends PropsWithChildren {
application?: RulesSourceApplication; application?: RulesSourceApplication;
isLoading?: boolean; isLoading?: boolean;
description?: ReactNode; description?: ReactNode;
error?: unknown;
} }
export const DataSourceSection = ({ export const DataSourceSection = ({
@ -30,6 +31,7 @@ export const DataSourceSection = ({
application, application,
children, children,
loader, loader,
error,
isLoading = false, isLoading = false,
description = null, description = null,
}: DataSourceSectionProps) => { }: DataSourceSectionProps) => {
@ -60,6 +62,7 @@ export const DataSourceSection = ({
name={isCollapsed ? 'angle-right' : 'angle-down'} name={isCollapsed ? 'angle-right' : 'angle-down'}
onClick={toggleCollapsed} onClick={toggleCollapsed}
aria-label={t('common.collapse', 'Collapse')} aria-label={t('common.collapse', 'Collapse')}
disabled={Boolean(error)}
/> />
{application && <DataSourceIcon application={application} />} {application && <DataSourceIcon application={application} />}
@ -71,6 +74,18 @@ export const DataSourceSection = ({
{'·'} {description} {'·'} {description}
</Text> </Text>
)} )}
{Boolean(error) && (
<Toggletip
title={t('alerting.rule-list.ds-error.title', 'Cannot load rules for this datasource')}
content={<div>{stringifyErrorLike(error)}</div>}
>
<Button variant="destructive" fill="outline" size="sm" icon="exclamation-circle">
<Trans i18nKey="alerting.rule-list.error-button">Error</Trans>
</Button>
</Toggletip>
)}
<Spacer /> <Spacer />
{configureLink && ( {configureLink && (
<WithReturnButton <WithReturnButton

@ -51,6 +51,7 @@ export function useLazyLoadPrometheusGroups<TGroup extends PromRuleGroupDTO>(
return { return {
isLoading, isLoading,
error: groupsRequestState.error,
groups, groups,
hasMoreGroups: !isLoading && hasMoreGroups, hasMoreGroups: !isLoading && hasMoreGroups,
fetchMoreGroups, fetchMoreGroups,

@ -2364,11 +2364,15 @@
"cannot-load-rule-details-for": "Cannot load rule details for UID {{uid}}", "cannot-load-rule-details-for": "Cannot load rule details for UID {{uid}}",
"configure-datasource": "Configure", "configure-datasource": "Configure",
"draft-new-rule": "Draft a new rule", "draft-new-rule": "Draft a new rule",
"ds-error": {
"title": "Cannot load rules for this datasource"
},
"ds-error-boundary": { "ds-error-boundary": {
"description": "Check the data source configuration. Does the data source support Prometheus API?", "description": "Check the data source configuration. Does the data source support Prometheus API?",
"title": "Unable to load rules from this data source" "title": "Unable to load rules from this data source"
}, },
"empty-data-source": "No rules found", "empty-data-source": "No rules found",
"error-button": "Error",
"filter-view": { "filter-view": {
"cancel-search": "Cancel search", "cancel-search": "Cancel search",
"no-more-results": "No more results – found {{numberOfRules}} rules", "no-more-results": "No more results – found {{numberOfRules}} rules",

Loading…
Cancel
Save