diff --git a/.betterer.results b/.betterer.results
index 694b989fa69..15b1090b3fe 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1232,9 +1232,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
- "public/app/features/alerting/unified/RuleList.tsx:5381": [
- [0, 0, 0, "Do not use any type assertions.", "0"]
- ],
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@@ -1299,6 +1296,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+ "public/app/features/alerting/unified/components/rule-list/RuleList.v1.tsx:5381": [
+ [0, 0, 0, "Do not use any type assertions.", "0"]
+ ],
"public/app/features/alerting/unified/components/silences/SilencesEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 127edc25de1..dcc598e8845 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -188,6 +188,7 @@ Experimental features might be changed or removed without prior notice.
| `autofixDSUID` | Automatically migrates invalid datasource UIDs |
| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore |
| `newDashboardSharingComponent` | Enables the new sharing drawer design |
+| `alertingListViewV2` | Enables the new alert list view design |
| `notificationBanner` | Enables the notification banner UI and API |
## Development feature toggles
diff --git a/package.json b/package.json
index d81c473269b..44d225eec44 100644
--- a/package.json
+++ b/package.json
@@ -393,6 +393,7 @@
"tinycolor2": "1.6.0",
"tslib": "2.6.2",
"tween-functions": "^1.2.0",
+ "type-fest": "^4.18.2",
"uplot": "1.6.30",
"uuid": "9.0.1",
"visjs-network": "4.25.0",
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 8792e5b55b9..3e450d7ca88 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -188,6 +188,7 @@ export interface FeatureToggles {
autofixDSUID?: boolean;
logsExploreTableDefaultVisualization?: boolean;
newDashboardSharingComponent?: boolean;
+ alertingListViewV2?: boolean;
notificationBanner?: boolean;
dashboardRestore?: boolean;
datasourceProxyDisableRBAC?: boolean;
diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts
index f88699833d8..ca8f4bb9bbd 100644
--- a/packages/grafana-data/src/types/icon.ts
+++ b/packages/grafana-data/src/types/icon.ts
@@ -185,6 +185,7 @@ export const availableIconsIndex = {
paragraph: true,
'pathfinder-unite': true,
pause: true,
+ 'pause-circle': true,
pen: true,
percentage: true,
play: true,
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index d62367f9821..f4dd16ad9ea 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1267,6 +1267,13 @@ var (
Owner: grafanaSharingSquad,
FrontendOnly: true,
},
+ {
+ Name: "alertingListViewV2",
+ Description: "Enables the new alert list view design",
+ Stage: FeatureStageExperimental,
+ Owner: grafanaAlertingSquad,
+ FrontendOnly: true,
+ },
{
Name: "notificationBanner",
Description: "Enables the notification banner UI and API",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index a2a8db91b7c..8cee18735f7 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -169,6 +169,7 @@ queryLibrary,experimental,@grafana/explore-squad,false,false,false
autofixDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,experimental,@grafana/sharing-squad,false,false,true
+alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true
notificationBanner,experimental,@grafana/grafana-frontend-platform,false,false,false
dashboardRestore,experimental,@grafana/grafana-frontend-platform,false,false,false
datasourceProxyDisableRBAC,GA,@grafana/identity-access-team,false,false,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 128f6d145cd..a49679849d1 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -687,6 +687,10 @@ const (
// Enables the new sharing drawer design
FlagNewDashboardSharingComponent = "newDashboardSharingComponent"
+ // FlagAlertingListViewV2
+ // Enables the new alert list view design
+ FlagAlertingListViewV2 = "alertingListViewV2"
+
// FlagNotificationBanner
// Enables the notification banner UI and API
FlagNotificationBanner = "notificationBanner"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index de752352db1..f8fdb103952 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -2237,6 +2237,19 @@
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
+ },
+ {
+ "metadata": {
+ "name": "alertingListViewV2",
+ "resourceVersion": "1716558084235",
+ "creationTimestamp": "2024-05-24T13:41:24Z"
+ },
+ "spec": {
+ "description": "Enables the new alert list view design",
+ "stage": "experimental",
+ "codeowner": "@grafana/alerting-squad",
+ "frontend": true
+ }
}
]
}
\ No newline at end of file
diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index 528febf8ebb..97bb8b7eece 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -162,7 +162,7 @@ const ui = {
paused: byText(/^Paused/),
},
actionButtons: {
- more: byRole('button', { name: /more-actions/ }),
+ more: byRole('button', { name: /More/ }),
},
moreActionItems: {
pause: byRole('menuitem', { name: /pause evaluation/i }),
diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx
index 3788ecb63f8..94dd35e71b5 100644
--- a/public/app/features/alerting/unified/RuleList.tsx
+++ b/public/app/features/alerting/unified/RuleList.tsx
@@ -1,185 +1,14 @@
-import { css } from '@emotion/css';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useLocation } from 'react-router-dom';
-import { useAsyncFn, useInterval } from 'react-use';
+import React, { Suspense } from 'react';
-import { GrafanaTheme2, urlUtil } from '@grafana/data';
-import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
-import { useQueryParams } from 'app/core/hooks/useQueryParams';
-import { useDispatch } from 'app/types';
+import { config } from '@grafana/runtime';
-import { CombinedRuleNamespace } from '../../../types/unified-alerting';
+import RuleListV1 from './components/rule-list/RuleList.v1';
+const RuleListV2 = React.lazy(() => import('./components/rule-list/RuleList.v2'));
-import { LogMessages, logInfo, trackRuleListNavigation } from './Analytics';
-import { AlertingPageWrapper } from './components/AlertingPageWrapper';
-import { NoRulesSplash } from './components/rules/NoRulesCTA';
-import { INSTANCES_DISPLAY_LIMIT } from './components/rules/RuleDetails';
-import { RuleListErrors } from './components/rules/RuleListErrors';
-import { RuleListGroupView } from './components/rules/RuleListGroupView';
-import { RuleListStateView } from './components/rules/RuleListStateView';
-import { RuleStats } from './components/rules/RuleStats';
-import RulesFilter from './components/rules/RulesFilter';
-import { AlertingAction, useAlertingAbility } from './hooks/useAbilities';
-import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
-import { useFilteredRules, useRulesFilter } from './hooks/useFilteredRules';
-import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
-import { fetchAllPromAndRulerRulesAction } from './state/actions';
-import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
-import { getAllRulesSourceNames } from './utils/datasource';
+const RuleList = () => {
+ const newView = config.featureToggles.alertingListViewV2;
-const VIEWS = {
- groups: RuleListGroupView,
- state: RuleListStateView,
+ return {newView ? : };
};
-// make sure we ask for 1 more so we show the "show x more" button
-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);
-
- const onFilterCleared = useCallback(() => setExpandAll(false), []);
-
- const [queryParams] = useQueryParams();
- const { filterState, hasActiveFilters } = useRulesFilter();
-
- const queryParamView = queryParams['view'] as keyof typeof VIEWS;
- const view = VIEWS[queryParamView] ? queryParamView : 'groups';
-
- const ViewComponent = VIEWS[view];
-
- const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
- const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
-
- const loading = rulesDataSourceNames.some(
- (name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading
- );
-
- const promRequests = Object.entries(promRuleRequests);
- const rulerRequests = Object.entries(rulerRuleRequests);
-
- const allPromLoaded = promRequests.every(
- ([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined)
- );
- const allRulerLoaded = rulerRequests.every(
- ([_, state]) => state.dispatched && (state?.result !== undefined || state?.error !== undefined)
- );
-
- const allPromEmpty = promRequests.every(([_, state]) => state.dispatched && state?.result?.length === 0);
-
- const allRulerEmpty = rulerRequests.every(([_, state]) => {
- const rulerRules = Object.entries(state?.result ?? {});
- const noRules = rulerRules.every(([_, result]) => result?.length === 0);
- return noRules && state.dispatched;
- });
-
- const limitAlerts = hasActiveFilters ? undefined : LIMIT_ALERTS;
- // Trigger data refresh only when the RULE_LIST_POLL_INTERVAL_MS elapsed since the previous load FINISHED
- const [_, fetchRules] = useAsyncFn(async () => {
- if (!loading) {
- await dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
- }
- }, [loading, limitAlerts, dispatch]);
-
- useEffect(() => {
- trackRuleListNavigation().catch(() => {});
- }, []);
-
- // fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
- useEffect(() => {
- dispatch(fetchAllPromAndRulerRulesAction(false, { limitAlerts }));
- }, [dispatch, limitAlerts]);
- useInterval(fetchRules, RULE_LIST_POLL_INTERVAL_MS);
-
- // Show splash only when we loaded all of the data sources and none of them has alerts
- const hasNoAlertRulesCreatedYet =
- allPromLoaded && allPromEmpty && promRequests.length > 0 && allRulerEmpty && allRulerLoaded;
- const hasAlertRulesCreated = !hasNoAlertRulesCreatedYet;
-
- const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces();
- const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState);
- return (
- // We don't want to show the Loading... indicator for the whole page.
- // We show separate indicators for Grafana-managed and Cloud rules
- }>
-
-
- {hasAlertRulesCreated && (
- <>
-
-
-
- {view === 'groups' && hasActiveFilters && (
-
- )}
-
-
-
- >
- )}
- {hasNoAlertRulesCreatedYet && }
- {hasAlertRulesCreated && }
-
- );
- },
- { 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() {
- const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
- const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
-
- const location = useLocation();
-
- const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
-
- const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
-
- if (canCreateGrafanaRules || canCreateCloudRules) {
- return (
- logInfo(LogMessages.alertRuleFromScratch)}
- >
- New alert rule
-
- );
- }
- return null;
-}
diff --git a/public/app/features/alerting/unified/components/Label.tsx b/public/app/features/alerting/unified/components/Label.tsx
index 7df0187a9ca..3d1b4d11d39 100644
--- a/public/app/features/alerting/unified/components/Label.tsx
+++ b/public/app/features/alerting/unified/components/Label.tsx
@@ -1,11 +1,11 @@
import { css } from '@emotion/css';
-import React, { ReactNode } from 'react';
+import React, { CSSProperties, ReactNode } from 'react';
import tinycolor2 from 'tinycolor2';
import { GrafanaTheme2, IconName } from '@grafana/data';
import { Icon, Stack, useStyles2 } from '@grafana/ui';
-export type LabelSize = 'md' | 'sm';
+export type LabelSize = 'md' | 'sm' | 'xs';
interface Props {
icon?: IconName;
@@ -58,8 +58,18 @@ const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
? tinycolor2.mostReadable(backgroundColor, ['#000', '#fff']).toString()
: theme.colors.text.primary;
- const padding =
- size === 'md' ? `${theme.spacing(0.33)} ${theme.spacing(1)}` : `${theme.spacing(0.2)} ${theme.spacing(0.6)}`;
+ let padding: CSSProperties['padding'] = theme.spacing(0.33, 1);
+
+ switch (size) {
+ case 'sm':
+ padding = theme.spacing(0.2, 0.6);
+ break;
+ case 'xs':
+ padding = theme.spacing(0, 0.5);
+ break;
+ default:
+ break;
+ }
return {
wrapper: css({
diff --git a/public/app/features/alerting/unified/components/MetaText.tsx b/public/app/features/alerting/unified/components/MetaText.tsx
index a1d564a5afa..4615d5ca2ac 100644
--- a/public/app/features/alerting/unified/components/MetaText.tsx
+++ b/public/app/features/alerting/unified/components/MetaText.tsx
@@ -27,7 +27,7 @@ const MetaText = ({ children, icon, color = 'secondary', direction = 'row', ...r
>
- {icon && }
+ {icon && }
{children}
diff --git a/public/app/features/alerting/unified/components/MoreButton.tsx b/public/app/features/alerting/unified/components/MoreButton.tsx
index 2fea06edbc3..7b41fe61fda 100644
--- a/public/app/features/alerting/unified/components/MoreButton.tsx
+++ b/public/app/features/alerting/unified/components/MoreButton.tsx
@@ -4,15 +4,7 @@ import { Button, ButtonProps, Icon, Stack } from '@grafana/ui';
const MoreButton = forwardRef(function MoreButton(props: ButtonProps, ref: Ref) {
return (
-