diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 22554bc065d..6b8e7e3bf35 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -3,7 +3,7 @@ import { render, waitFor } from '@testing-library/react'; import { configureStore } from 'app/store/configureStore'; import { Provider } from 'react-redux'; import { RuleList } from './RuleList'; -import { byTestId, byText } from 'testing-library-selector'; +import { byRole, byTestId, byText } from 'testing-library-selector'; import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; import { getAllDataSources } from './utils/config'; import { fetchRules } from './api/prometheus'; @@ -77,6 +77,7 @@ const ui = { ruleRow: byTestId('row'), expandedContent: byTestId('expanded-content'), rulesFilterInput: byTestId('search-query-input'), + moreErrorsButton: byRole('button', { name: /more errors/ }), }; describe('RuleList', () => { @@ -164,6 +165,10 @@ describe('RuleList', () => { const errors = await ui.cloudRulesSourceErrors.find(); + expect(errors).not.toHaveTextContent( + 'Failed to load rules state from Prometheus-broken: this datasource is broken' + ); + userEvent.click(ui.moreErrorsButton.get()); expect(errors).toHaveTextContent('Failed to load rules state from Prometheus-broken: this datasource is broken'); }); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 14d5f535605..dae426394e6 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -1,6 +1,5 @@ -import { DataSourceInstanceSettings, GrafanaTheme, urlUtil } from '@grafana/data'; -import { useStyles, Alert, LinkButton, withErrorBoundary } from '@grafana/ui'; -import { SerializedError } from '@reduxjs/toolkit'; +import { GrafanaTheme2, urlUtil } from '@grafana/data'; +import { useStyles2, LinkButton, withErrorBoundary } from '@grafana/ui'; import React, { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; @@ -8,11 +7,10 @@ import { NoRulesSplash } from './components/rules/NoRulesCTA'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { useFilteredRules } from './hooks/useFilteredRules'; import { fetchAllPromAndRulerRulesAction } from './state/actions'; -import { getAllRulesSourceNames, getRulesDataSources, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { getAllRulesSourceNames } from './utils/datasource'; import { css } from '@emotion/css'; import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; -import { isRulerNotSupportedResponse } from './utils/rules'; import RulesFilter from './components/rules/RulesFilter'; import { RuleListGroupView } from './components/rules/RuleListGroupView'; import { RuleListStateView } from './components/rules/RuleListStateView'; @@ -20,6 +18,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useLocation } from 'react-router-dom'; import { contextSrv } from 'app/core/services/context_srv'; import { RuleStats } from './components/rules/RuleStats'; +import { RuleListErrors } from './components/rules/RuleListErrors'; const VIEWS = { groups: RuleListGroupView, @@ -29,7 +28,7 @@ const VIEWS = { export const RuleList = withErrorBoundary( () => { const dispatch = useDispatch(); - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); const location = useLocation(); @@ -65,54 +64,13 @@ export const RuleList = withErrorBoundary( (Object.keys(rulerRuleRequests[name]?.result || {}).length && !rulerRuleRequests[name]?.error) ); - const [promReqeustErrors, rulerRequestErrors] = useMemo( - () => - [promRuleRequests, rulerRuleRequests].map((requests) => - getRulesDataSources().reduce>( - (result, dataSource) => { - const error = requests[dataSource.name]?.error; - if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) { - return [...result, { dataSource, error }]; - } - return result; - }, - [] - ) - ), - [promRuleRequests, rulerRuleRequests] - ); - - const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; - const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; - const showNewAlertSplash = dispatched && !loading && !haveResults; const combinedNamespaces = useCombinedRuleNamespaces(); const filteredNamespaces = useFilteredRules(combinedNamespaces); return ( - {(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && ( - - {grafanaPromError && ( -
Failed to load Grafana rules state: {grafanaPromError.message || 'Unknown error.'}
- )} - {grafanaRulerError && ( -
Failed to load Grafana rules config: {grafanaRulerError.message || 'Unknown error.'}
- )} - {promReqeustErrors.map(({ dataSource, error }) => ( -
- Failed to load rules state from {dataSource.name}:{' '} - {error.message || 'Unknown error.'} -
- ))} - {rulerRequestErrors.map(({ dataSource, error }) => ( -
- Failed to load rules config from {dataSource.name}:{' '} - {error.message || 'Unknown error.'} -
- ))} -
- )} + {!showNewAlertSplash && ( <> @@ -139,19 +97,15 @@ export const RuleList = withErrorBoundary( { style: 'page' } ); -const getStyles = (theme: GrafanaTheme) => ({ +const getStyles = (theme: GrafanaTheme2) => ({ break: css` width: 100%; height: 0; - margin-bottom: ${theme.spacing.md}; - border-bottom: solid 1px ${theme.colors.border2}; - `, - iconError: css` - color: ${theme.palette.red}; - margin-right: ${theme.spacing.md}; + margin-bottom: ${theme.spacing(2)}; + border-bottom: solid 1px ${theme.colors.border.medium}; `, buttonsContainer: css` - margin-bottom: ${theme.spacing.md}; + margin-bottom: ${theme.spacing(2)}; display: flex; justify-content: space-between; `, diff --git a/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx b/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx new file mode 100644 index 00000000000..1c26499d838 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx @@ -0,0 +1,94 @@ +import { css } from '@emotion/css'; +import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; +import { Alert, Button, useStyles2 } from '@grafana/ui'; +import { SerializedError } from '@reduxjs/toolkit'; +import pluralize from 'pluralize'; +import React, { useMemo, ReactElement, useState } from 'react'; +import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; +import { getRulesDataSources, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { isRulerNotSupportedResponse } from '../../utils/rules'; + +export function RuleListErrors(): ReactElement { + const [expanded, setExpanded] = useState(false); + const [closed, setClosed] = useState(false); + const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules); + const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); + const styles = useStyles2(getStyles); + + const errors = useMemo((): JSX.Element[] => { + const [promRequestErrors, rulerRequestErrors] = [promRuleRequests, rulerRuleRequests].map((requests) => + getRulesDataSources().reduce>( + (result, dataSource) => { + const error = requests[dataSource.name]?.error; + if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) { + return [...result, { dataSource, error }]; + } + return result; + }, + [] + ) + ); + const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; + const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; + + const result: JSX.Element[] = []; + + if (grafanaPromError) { + result.push(<>Failed to load Grafana rules state: {grafanaPromError.message || 'Unknown error.'}); + } + if (grafanaRulerError) { + result.push(<>Failed to load Grafana rules config: {grafanaRulerError.message || 'Unknown error.'}); + } + + promRequestErrors.forEach(({ dataSource, error }) => + result.push( + <> + Failed to load rules state from {dataSource.name}:{' '} + {error.message || 'Unknown error.'} + + ) + ); + + rulerRequestErrors.forEach(({ dataSource, error }) => + result.push( + <> + Failed to load rules config from {dataSource.name}:{' '} + {error.message || 'Unknown error.'} + + ) + ); + + return result; + }, [promRuleRequests, rulerRuleRequests]); + + return ( + <> + {errors.length && !closed && ( + setClosed(true)} + > + {expanded && errors.map((item, idx) =>
{item}
)} + {!expanded && ( + <> +
{errors[0]}
+ {errors.length >= 2 && ( + + )} + + )} +
+ )} + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + moreButton: css` + padding: 0; + `, +});