From 082b46067c2ace84d80012d9846b8e5e94aba2a1 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Tue, 20 May 2025 18:03:07 +0200 Subject: [PATCH] Alerting: New list view layout update (#105489) --- .../DeleteModal.tsx | 0 .../FolderActionsButton.tsx} | 57 +++++-- .../PauseUnpauseActionMenuItem.tsx | 4 +- .../components/rule-viewer/AlertRuleMenu.tsx | 10 +- .../components/rule-viewer/RuleViewer.tsx | 1 + .../unified/components/rules/RulesGroup.tsx | 2 +- .../unified/rule-list/GroupedView.test.tsx | 50 +++--- .../rule-list/PaginatedDataSourceLoader.tsx | 36 ++-- .../rule-list/PaginatedGrafanaLoader.tsx | 53 +++--- .../components/AlertRuleListItem.tsx | 2 +- .../components/DataSourceSection.tsx | 27 +-- .../rule-list/components/LazyPagination.tsx | 36 ---- .../rule-list/components/ListGroup.tsx | 34 +++- .../unified/rule-list/components/ListItem.tsx | 14 +- .../rule-list/components/ListSection.tsx | 31 ++-- .../rule-list/components/LoadMoreButton.tsx | 17 ++ .../components/RuleActionsButtons.V2.tsx | 5 +- .../components/RuleGroupActionsMenu.tsx | 156 ------------------ .../rule-list/components/RuleListIcon.tsx | 18 +- .../hooks/useLazyLoadPrometheusGroups.tsx | 58 +++++++ .../hooks/usePaginatedPrometheusGroups.tsx | 77 --------- .../grafana-azure-monitor-datasource.json | 4 +- public/locales/en-US/grafana.json | 25 +-- 23 files changed, 284 insertions(+), 433 deletions(-) rename public/app/features/alerting/unified/components/{folder-bulk-actions => folder-actions}/DeleteModal.tsx (100%) rename public/app/features/alerting/unified/components/{folder-bulk-actions/FolderBulkActionsButton.tsx => folder-actions/FolderActionsButton.tsx} (76%) rename public/app/features/alerting/unified/components/{folder-bulk-actions => folder-actions}/PauseUnpauseActionMenuItem.tsx (96%) delete mode 100644 public/app/features/alerting/unified/rule-list/components/LazyPagination.tsx create mode 100644 public/app/features/alerting/unified/rule-list/components/LoadMoreButton.tsx delete mode 100644 public/app/features/alerting/unified/rule-list/components/RuleGroupActionsMenu.tsx create mode 100644 public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx delete mode 100644 public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx diff --git a/public/app/features/alerting/unified/components/folder-bulk-actions/DeleteModal.tsx b/public/app/features/alerting/unified/components/folder-actions/DeleteModal.tsx similarity index 100% rename from public/app/features/alerting/unified/components/folder-bulk-actions/DeleteModal.tsx rename to public/app/features/alerting/unified/components/folder-actions/DeleteModal.tsx diff --git a/public/app/features/alerting/unified/components/folder-bulk-actions/FolderBulkActionsButton.tsx b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx similarity index 76% rename from public/app/features/alerting/unified/components/folder-bulk-actions/FolderBulkActionsButton.tsx rename to public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx index d78237a043b..373ae7a91d3 100644 --- a/public/app/features/alerting/unified/components/folder-bulk-actions/FolderBulkActionsButton.tsx +++ b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx @@ -12,6 +12,7 @@ import { useFolder } from '../../hooks/useFolder'; import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../../state/actions'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { createRelativeUrl } from '../../utils/url'; +import MoreButton from '../MoreButton'; import { DeleteModal } from './DeleteModal'; import { PauseUnpauseActionMenuItem } from './PauseUnpauseActionMenuItem'; @@ -20,20 +21,30 @@ interface Props { } export const FolderBulkActionsButton = ({ folderUID }: Props) => { + const { t } = useTranslate(); + + // state + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + // abilities const [pauseSupported, pauseAllowed] = useFolderBulkActionAbility(FolderBulkAction.Pause); - const canPause = pauseSupported && pauseAllowed; const [deleteSupported, deleteAllowed] = useFolderBulkActionAbility(FolderBulkAction.Delete); + + const canPause = pauseSupported && pauseAllowed; const canDelete = deleteSupported && deleteAllowed; + + // mutations const [pauseFolder, updateState] = alertingFolderActionsApi.endpoints.pauseFolder.useMutation(); const [unpauseFolder, unpauseState] = alertingFolderActionsApi.endpoints.unpauseFolder.useMutation(); const [deleteGrafanaRulesFromFolder, deleteState] = alertingFolderActionsApi.endpoints.deleteGrafanaRulesFromFolder.useMutation(); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const folderName = useFolder(folderUID).folder?.title || 'unknown folder'; - const { t } = useTranslate(); const listView2Enabled = config.featureToggles.alertingListViewV2 ?? false; - const view = listView2Enabled ? 'list' : 'grouped'; - const redirectToListView = useRedirectToListView(view); + const viewComponent = listView2Enabled ? 'list' : 'grouped'; + + // URLs + const redirectToListView = useRedirectToListView(viewComponent); if (!canPause && !canDelete) { return null; @@ -70,24 +81,44 @@ export const FolderBulkActionsButton = ({ folderUID }: Props) => { )} {canDelete && ( setIsDeleteModalOpen(true)} disabled={deleteState.isLoading} /> )} + {/* @TODO re-implement */} + {/* {listView2Enabled && ( + <> + + {}} + /> + + )} */} ); return ( <> - {menuItems}}> - + {menuItems}}> + {listView2Enabled ? ( + + ) : ( + + )} void; onPauseChange?: () => void; buttonSize?: ComponentSize; + fill?: PropsOf['fill']; } /** @@ -41,6 +44,7 @@ const AlertRuleMenu = ({ handleDuplicateRule, onPauseChange, buttonSize, + fill, }: Props) => { // check all abilities and permissions const [pauseSupported, pauseAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Pause); @@ -145,8 +149,8 @@ const AlertRuleMenu = ({ ); return ( - {menuItems}}> - + {menuItems}} placement="bottom"> + ); }; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 5cd208d36f7..32b648243b9 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -117,6 +117,7 @@ const RuleViewer = () => { health={promRule?.health} ruleType={promRule?.type} ruleOrigin={ruleOrigin} + returnToHref="/alerting/list" /> )} actions={} diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index 698f3b20cc3..3bf79f45c0e 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -19,7 +19,7 @@ import { CollapseToggle } from '../CollapseToggle'; import { RuleLocation } from '../RuleLocation'; import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter'; import { decodeGrafanaNamespace } from '../expressions/util'; -import { FolderBulkActionsButton } from '../folder-bulk-actions/FolderBulkActionsButton'; +import { FolderBulkActionsButton } from '../folder-actions/FolderActionsButton'; import { ActionIcon } from './ActionIcon'; import { RuleGroupStats } from './RuleStats'; diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index 953bce50ead..54eb489cb73 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -10,6 +10,7 @@ import { setPrometheusRules } from '../mocks/server/configure'; import { alertingFactory } from '../mocks/server/db'; import { GroupedView } from './GroupedView'; +import { DATA_SOURCE_GROUP_PAGE_SIZE } from './PaginatedDataSourceLoader'; setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); @@ -35,7 +36,7 @@ const ui = { dsSection: (ds: string | RegExp) => byRole('listitem', { name: ds }), namespace: (ns: string | RegExp) => byRole('treeitem', { name: ns }), group: (group: string | RegExp) => byRole('treeitem', { name: group }), - nextButton: () => byRole('button', { name: /next page/ }), + loadMoreButton: () => byRole('button', { name: /Show more/i }), }; describe('RuleList - GroupedView', () => { @@ -64,14 +65,14 @@ describe('RuleList - GroupedView', () => { expect(firstPageGroups[24]).toHaveTextContent('test-group-25'); expect(firstPageGroups[39]).toHaveTextContent('test-group-40'); - const nextButton = await within(mimirSection).findByRole('button', { name: /next page/ }); - await user.click(nextButton); + const loadMoreButton = await within(mimirSection).findByRole('button', { name: /Show more/i }); + await user.click(loadMoreButton); - await waitFor(() => expect(nextButton).toBeEnabled()); + await waitFor(() => expect(loadMoreButton).toBeEnabled()); const secondPageGroups = await ui.group(/test-group-(4[1-9]|[5-7][0-9]|80)/).findAll(mimirNamespace); - expect(secondPageGroups).toHaveLength(40); + expect(secondPageGroups).toHaveLength(DATA_SOURCE_GROUP_PAGE_SIZE); expect(secondPageGroups[0]).toHaveTextContent('test-group-41'); expect(secondPageGroups[24]).toHaveTextContent('test-group-65'); expect(secondPageGroups[39]).toHaveTextContent('test-group-80'); @@ -81,28 +82,25 @@ describe('RuleList - GroupedView', () => { const { user } = render(); const prometheusSection = await ui.dsSection(/Prometheus/).find(); - - const nextButton = await ui.nextButton().find(prometheusSection); - await waitFor(() => expect(nextButton).toBeEnabled()); - - // Fetch second page - await user.click(nextButton); - - // Fetch third page - await waitFor(() => expect(nextButton).toBeEnabled()); - await user.click(nextButton); - - // Fetch fourth page - await waitFor(() => expect(nextButton).toBeEnabled(), { timeout: 10000 }); - await user.click(nextButton); - const promNamespace = await ui.namespace(/test-prometheus-namespace/).find(prometheusSection); - const lastPageGroups = await ui.group(/test-group-(12[1-9]|130)/).findAll(promNamespace); - expect(lastPageGroups).toHaveLength(10); - expect(lastPageGroups.at(0)).toHaveTextContent('test-group-121'); - expect(lastPageGroups.at(6)).toHaveTextContent('test-group-127'); - expect(lastPageGroups.at(9)).toHaveTextContent('test-group-130'); - expect(nextButton).toBeDisabled(); + // initial load – should have all groups 1-40 + await ui.group(/test-group-([1-9]|[1-3][0-9]|40)/).findAll(promNamespace); + + // fetch page 2 + const loadMoreButton = await ui.loadMoreButton().find(prometheusSection); + await waitFor(() => expect(loadMoreButton).toBeEnabled()); + + // we should now have all groups 1-80 + await ui.group(/test-group-([1-9]|[1-7][0-9]|80)/).findAll(promNamespace); + + // fetch third page + await waitFor(() => expect(loadMoreButton).toBeEnabled()); + await user.click(loadMoreButton); + + // we should now have all groups 1-130 + await ui.group(/test-group-([1-9]|[1-9][0-9]|1[0-2][0-9]|130)/).findAll(promNamespace); + + expect(loadMoreButton).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index d9368b37f0a..ddd82a1eb56 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -8,14 +8,13 @@ import { groups } from '../utils/navigation'; import { DataSourceGroupLoader } from './DataSourceGroupLoader'; import { DataSourceSection, DataSourceSectionProps } from './components/DataSourceSection'; -import { LazyPagination } from './components/LazyPagination'; import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; -import { RuleGroupActionsMenu } from './components/RuleGroupActionsMenu'; +import { LoadMoreButton } from './components/LoadMoreButton'; import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator'; -import { usePaginatedPrometheusGroups } from './hooks/usePaginatedPrometheusGroups'; +import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; -const DATA_SOURCE_GROUP_PAGE_SIZE = 40; +export const DATA_SOURCE_GROUP_PAGE_SIZE = 40; interface PaginatedDataSourceLoaderProps extends Required> { rulesSourceIdentifier: DataSourceRulesSourceIdentifier; @@ -36,20 +35,16 @@ export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application } }; }, [groupsGenerator]); - const { - page: groupsPage, - nextPage, - previousPage, - canMoveForward, - canMoveBackward, - isLoading, - } = usePaginatedPrometheusGroups(groupsGenerator.current, DATA_SOURCE_GROUP_PAGE_SIZE); + const { isLoading, groups, hasMoreGroups, fetchMoreGroups } = useLazyLoadPrometheusGroups( + groupsGenerator.current, + DATA_SOURCE_GROUP_PAGE_SIZE + ); - const groupsByNamespace = useMemo(() => groupBy(groupsPage, 'file'), [groupsPage]); + const groupsByNamespace = useMemo(() => groupBy(groups, 'file'), [groups]); return ( - + {Object.entries(groupsByNamespace).map(([namespace, groups]) => ( ))} - + {hasMoreGroups && ( + // this div will make the button not stretch +
+ +
+ )}
); @@ -106,7 +101,6 @@ function RuleGroupListItem({ rulesSourceIdentifier, group, namespaceName }: Rule name={group.name} href={groups.detailsPageLink(rulesSourceIdentifier.uid, namespaceName, group.name)} isOpen={false} - actions={} > diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index bfabaa16cff..4896cf5675d 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -1,25 +1,26 @@ import { groupBy } from 'lodash'; import { useEffect, useMemo, useRef } from 'react'; +import { Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { Icon, Stack, Text } from '@grafana/ui'; +import { Icon, LinkButton, Stack, Text } from '@grafana/ui'; import { GrafanaRuleGroupIdentifier, GrafanaRulesSourceSymbol } from 'app/types/unified-alerting'; import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto'; -import { FolderBulkActionsButton } from '../components/folder-bulk-actions/FolderBulkActionsButton'; +import { FolderBulkActionsButton } from '../components/folder-actions/FolderActionsButton'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { makeFolderLink } from '../utils/misc'; import { groups } from '../utils/navigation'; import { GrafanaGroupLoader } from './GrafanaGroupLoader'; import { DataSourceSection } from './components/DataSourceSection'; -import { LazyPagination } from './components/LazyPagination'; import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; -import { RuleGroupActionsMenu } from './components/RuleGroupActionsMenu'; +import { LoadMoreButton } from './components/LoadMoreButton'; import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; -import { usePaginatedPrometheusGroups } from './hooks/usePaginatedPrometheusGroups'; +import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; -const GRAFANA_GROUP_PAGE_SIZE = 40; +export const GRAFANA_GROUP_PAGE_SIZE = 40; export function PaginatedGrafanaLoader() { const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ populateCache: true }); @@ -33,25 +34,23 @@ export function PaginatedGrafanaLoader() { }; }, []); - const { - page: groupsPage, - nextPage, - previousPage, - canMoveForward, - canMoveBackward, - isLoading, - } = usePaginatedPrometheusGroups(groupsGenerator.current, GRAFANA_GROUP_PAGE_SIZE); + const { isLoading, groups, hasMoreGroups, fetchMoreGroups } = useLazyLoadPrometheusGroups( + groupsGenerator.current, + GRAFANA_GROUP_PAGE_SIZE + ); - const groupsByFolder = useMemo(() => groupBy(groupsPage, 'folderUid'), [groupsPage]); + const groupsByFolder = useMemo(() => groupBy(groups, 'folderUid'), [groups]); const isFolderBulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; return ( - + {Object.entries(groupsByFolder).map(([folderUid, groups]) => { // Groups are grouped by folder, so we can use the first group to get the folder name const folderName = groups[0].file; + const folderUrl = makeFolderLink(folderUid); + return ( } - actions={isFolderBulkActionsEnabled ? : null} + actions={ + <> + + View folder + + {isFolderBulkActionsEnabled ? : null} + + } > {groups.map((group) => ( ); })} - + {hasMoreGroups && ( + // this div will make the button not stretch +
+ +
+ )}
); @@ -109,7 +115,6 @@ export function GrafanaRuleGroupListItem({ group, namespaceName }: GrafanaRuleGr name={group.name} href={groups.detailsPageLink(GRAFANA_RULES_SOURCE_NAME, group.folderUid, group.name)} isOpen={false} - actions={} > diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx index d965a692c0e..70942051431 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx @@ -181,7 +181,7 @@ export function RecordingRuleListItem({ - + {name} {origin && } diff --git a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx index 8cf1509cd9f..1579cc236d5 100644 --- a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx @@ -35,14 +35,14 @@ export const DataSourceSection = ({ isLoading = false, description = null, }: DataSourceSectionProps) => { - const styles = useStyles2(getStyles); + const [isCollapsed, toggleCollapsed] = useToggle(false); + const styles = useStyles2((theme) => getStyles(theme, isCollapsed)); const { rulesSourcesWithRuler } = useRulesSourcesWithRuler(); const showImportLink = uid !== GrafanaRulesSourceSymbol && rulesSourcesWithRuler.some(({ uid: dsUid, type }) => dsUid === uid && supportedImportTypes.includes(type)); - const [isCollapsed, toggleCollapsed] = useToggle(false); const { t } = useTranslate(); const configureLink = (() => { if (uid === GrafanaRulesSourceSymbol) { @@ -56,7 +56,7 @@ export const DataSourceSection = ({ })(); return (
- + {isLoading && }
@@ -82,9 +82,9 @@ export const DataSourceSection = ({ {showImportLink && ( Import to Grafana rules @@ -93,7 +93,7 @@ export const DataSourceSection = ({ + Configure } @@ -109,25 +109,12 @@ export const DataSourceSection = ({ ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2, isCollapsed = false) => ({ itemsWrapper: css({ position: 'relative', - marginLeft: theme.spacing(1.5), - - '&:before': { - content: "''", - position: 'absolute', - height: '100%', - - marginLeft: `-${theme.spacing(1.5)}`, - borderLeft: `solid 1px ${theme.colors.border.weak}`, - }, }), dataSourceSectionTitle: css({ background: theme.colors.background.secondary, - padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`, - - border: `solid 1px ${theme.colors.border.weak}`, - borderRadius: theme.shape.radius.default, + padding: theme.spacing(1, 1.5), }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/LazyPagination.tsx b/public/app/features/alerting/unified/rule-list/components/LazyPagination.tsx deleted file mode 100644 index 1008ab98840..00000000000 --- a/public/app/features/alerting/unified/rule-list/components/LazyPagination.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslate } from '@grafana/i18n'; -import { Button, Icon, Stack } from '@grafana/ui'; - -interface LazyPaginationProps { - canMoveForward: boolean; - canMoveBackward: boolean; - nextPage: () => void; - previousPage: () => void; -} - -export function LazyPagination({ canMoveForward, canMoveBackward, nextPage, previousPage }: LazyPaginationProps) { - const { t } = useTranslate(); - - return ( - - - - - ); -} diff --git a/public/app/features/alerting/unified/rule-list/components/ListGroup.tsx b/public/app/features/alerting/unified/rule-list/components/ListGroup.tsx index 095087c3919..b4cdad759e0 100644 --- a/public/app/features/alerting/unified/rule-list/components/ListGroup.tsx +++ b/public/app/features/alerting/unified/rule-list/components/ListGroup.tsx @@ -40,7 +40,11 @@ export const ListGroup = ({ actions={actions} href={href} /> - {open &&
{children}
} + {open && ( +
+ {children} +
+ )}
); }; @@ -57,7 +61,7 @@ const GroupHeader = (props: GroupHeaderProps) => { return (
- + ({ groupWrapper: css({ display: 'flex', flexDirection: 'column', + position: 'relative', + + '&:before': { + content: "''", + position: 'absolute', + height: '100%', + + marginLeft: theme.spacing(2.5), + borderLeft: `solid 1px ${theme.colors.border.weak}`, + }, }), headerWrapper: css({ - padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + padding: theme.spacing(1), + paddingLeft: theme.spacing(4), + position: 'relative', - background: theme.colors.background.secondary, - - border: 'none', - borderBottom: `solid 1px ${theme.colors.border.weak}`, - borderTopLeftRadius: theme.shape.radius.default, - borderTopRightRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.action.hover, + }, + }), + childrenWrapper: css({ + position: 'relative', }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/ListItem.tsx b/public/app/features/alerting/unified/rule-list/components/ListItem.tsx index 85c3a0063ef..ce3ea7f6fb6 100644 --- a/public/app/features/alerting/unified/rule-list/components/ListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/components/ListItem.tsx @@ -29,7 +29,7 @@ export const ListItem = (props: ListItemProps) => { > {/* icon */} - {icon} + {icon} {/* title */} @@ -80,14 +80,20 @@ const getStyles = (theme: GrafanaTheme2) => ({ alertListItemContainer: css({ position: 'relative', listStyle: 'none', - background: theme.colors.background.primary, - borderBottom: `solid 1px ${theme.colors.border.weak}`, - padding: `${theme.spacing(1)} ${theme.spacing(1)}`, + padding: theme.spacing(1), + + '&:hover': { + background: theme.colors.action.hover, + }, }), textOverflow: css({ overflow: 'hidden', textOverflow: 'ellipsis', color: 'inherit', }), + // this will line up the icon with the title of the rule + statusIcon: css({ + marginTop: theme.spacing(0.5), + }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx index ecbbb32414d..e9ff9761dd9 100644 --- a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx @@ -30,7 +30,7 @@ export const ListSection = ({
  • - + ({ groupItemsWrapper: css({ position: 'relative', - borderRadius: theme.shape.radius.default, - border: `solid 1px ${theme.colors.border.weak}`, - borderBottom: 'none', - marginLeft: theme.spacing(1.5), + // unfortunately we have to resort to this since we can't overwrite the styles of the list items individually + // unless we clone the React Elements and modify className + 'li[role=treeitem]': { + paddingLeft: theme.spacing(6.5), + + '&:before': { + content: "''", + position: 'absolute', + height: '100%', + + marginLeft: theme.spacing(-1.5), + marginTop: theme.spacing(-1), + borderLeft: `solid 1px ${theme.colors.border.weak}`, + }, + }, }), wrapper: css({ display: 'flex', flexDirection: 'column', - - gap: theme.spacing(1), }), sectionTitle: css({ - padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + padding: theme.spacing(1, 1.5), - background: theme.colors.background.secondary, - border: `solid 1px ${theme.colors.border.weak}`, - borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.action.hover, + }, }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/LoadMoreButton.tsx b/public/app/features/alerting/unified/rule-list/components/LoadMoreButton.tsx new file mode 100644 index 00000000000..e77f783c890 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/LoadMoreButton.tsx @@ -0,0 +1,17 @@ +import { useTranslate } from '@grafana/i18n'; +import { Button } from '@grafana/ui'; + +interface LoadMoreButtonProps { + onClick: () => void; +} + +export function LoadMoreButton({ onClick }: LoadMoreButtonProps) { + const { t } = useTranslate(); + const label = t('alerting.rule-list.pagination.next-page', 'Show more…'); + + return ( + + ); +} diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx index 8222cae9eef..06387758bd4 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx @@ -58,7 +58,7 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: size={buttonSize} key="edit" variant="secondary" - icon="pen" + fill="text" href={editURL} > Edit @@ -67,10 +67,11 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: } return ( - + {buttons} ; - case 'datasource': - return ; - default: - return null; - } -} - -function DataSourceGroupsActionMenu({ groupIdentifier }: DataSourceGroupsActionMenuProps) { - const { canEditRules } = useRulesAccess(); - const { data: dataSourceInfo } = useDiscoverDsFeaturesQuery({ uid: groupIdentifier.rulesSource.uid }); - - const { - data: rulerRuleGroup, - error: rulerGroupError, - isLoading: isRulerGroupLoading, - } = useGetRuleGroupForNamespaceQuery( - dataSourceInfo?.rulerConfig - ? { - namespace: groupIdentifier.namespace.name, - group: groupIdentifier.groupName, - rulerConfig: dataSourceInfo?.rulerConfig!, - } - : skipToken - ); - const { t } = useTranslate(); - - const isFederated = rulerRuleGroup ? isFederatedRuleGroup(rulerRuleGroup) : false; - const isPluginProvided = rulerRuleGroup ? isPluginProvidedGroup(rulerRuleGroup) : false; - - const canEdit = !isFederated && !isPluginProvided && canEditRules(groupIdentifier.rulesSource.name); - const rulesSource = getRulesDataSourceByUID(groupIdentifier.rulesSource.uid); - - if (!rulesSource) { - // This should never happen - return null; - } - - // We don't provide any actions if the data source doesn't support ruler - if (!dataSourceInfo?.rulerConfig) { - return null; - } - - if (isRulerGroupLoading) { - return ; - } - - if (rulerGroupError) { - if (isFetchError(rulerGroupError) && rulerGroupError.status === 404) { - return ; - } - - return ( - - ); - } - - // This should never happen. Loading and error states are handled above - if (!rulerRuleGroup) { - return ; - } - - return ( - - - {canEdit && ( - - )} - - } - > - - - ); -} - -function GrafanaGroupsActionMenu({ groupIdentifier }: GrafanaGroupsActionMenuProps) { - const { canEditRules } = useRulesAccess(); - const { data: rulerRuleGroup } = useGetGrafanaRulerGroupQuery({ - folderUid: groupIdentifier.namespace.uid, - groupName: groupIdentifier.groupName, - }); - - const isProvisioned = rulerRuleGroup ? isProvisionedRuleGroup(rulerRuleGroup) : false; - const isPluginProvided = rulerRuleGroup ? isPluginProvidedGroup(rulerRuleGroup) : false; - - const folderUid = groupIdentifier.namespace.uid; - const { folder } = useFolder(folderUid); - const { t } = useTranslate(); - const canEdit = folder?.canSave && !isProvisioned && !isPluginProvided && canEditRules(GRAFANA_RULES_SOURCE_NAME); - - if (!canEdit) { - return null; - } - - return ( - - {t('alerting.group-actions.edit', 'Edit')} - - ); -} diff --git a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx index f9a4de439d6..96025385826 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx @@ -54,7 +54,7 @@ const operationIcons: Record = { }; // ⚠️ not trivial to update this, you have to re-do the math for the loading spinner -const ICON_SIZE = 18; +const ICON_SIZE = 15; /** * Make sure that the order of importance here matches the one we use in the StateBadge component for the detail view @@ -109,7 +109,7 @@ export const RuleListIcon = memo(function RuleListIcon({
    - + {/* this loading spinner works by using an optical illusion; the actual icon is static and the "spinning" part is just a semi-transparent darker circle overlayed on top. This makes it look like there is a small bright colored spinner rotating. @@ -118,22 +118,22 @@ export const RuleListIcon = memo(function RuleListIcon({ )} @@ -159,8 +159,8 @@ const spin = keyframes({ const getStyles = (theme: GrafanaTheme2) => ({ iconsContainer: css({ position: 'relative', - width: 18, - height: 18, + width: ICON_SIZE, + height: ICON_SIZE, '> *': { position: 'absolute', }, diff --git a/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx new file mode 100644 index 00000000000..351e42427d0 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { useEffectOnce } from 'react-use'; + +import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; + +import { isLoading as isLoadingState, useAsync } from '../../hooks/useAsync'; + +/** + * Provides lazy loading for rule groups. + * Instead of loading all groups at once, it uses a generator to fetch them in batches as needed, + * which helps with performance when dealing with large numbers of rules. + * + * @param groupsGenerator - An async generator that yields rule groups in batches + * @param pageSize - Number of groups to display per page + * @returns Groups loaded so far and controls for navigating through rule groups + */ +export function useLazyLoadPrometheusGroups( + groupsGenerator: AsyncIterator, + pageSize: number +) { + const [groups, setGroups] = useState([]); + const [hasMoreGroups, setHasMoreGroups] = useState(true); + + const [{ execute: fetchMoreGroups }, groupsRequestState] = useAsync(async () => { + let done = false; + const currentGroups: TGroup[] = []; + + while (currentGroups.length < pageSize) { + const generatorResult = await groupsGenerator.next(); + if (generatorResult.done) { + done = true; + break; + } + const group = generatorResult.value; + currentGroups.push(group); + } + + if (done) { + setHasMoreGroups(false); + } + + setGroups((groups) => groups.concat(currentGroups)); + }); + + // make sure we only load the initial group exactly once + useEffectOnce(() => { + fetchMoreGroups(); + }); + + const isLoading = isLoadingState(groupsRequestState); + + return { + isLoading, + groups, + hasMoreGroups: !isLoading && hasMoreGroups, + fetchMoreGroups, + }; +} diff --git a/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx b/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx deleted file mode 100644 index 18da7186fb5..00000000000 --- a/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; - -import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; - -import { isLoading, useAsync } from '../../hooks/useAsync'; - -/** - * Provides pagination functionality for rule groups with lazy loading. - * Instead of loading all groups at once, it uses a generator to fetch them in batches as needed, - * which helps with performance when dealing with large numbers of rules. - * - * @param groupsGenerator - An async generator that yields rule groups in batches - * @param pageSize - Number of groups to display per page - * @returns Pagination state and controls for navigating through rule groups - */ -export function usePaginatedPrometheusGroups( - groupsGenerator: AsyncIterator, - pageSize: number -) { - const [currentPage, setCurrentPage] = useState(1); - const [groups, setGroups] = useState([]); - const [lastPage, setLastPage] = useState(undefined); - - const [{ execute: fetchMoreGroups }, groupsRequestState] = useAsync(async (groupsCount: number) => { - let done = false; - const currentGroups: TGroup[] = []; - - while (currentGroups.length < groupsCount) { - const generatorResult = await groupsGenerator.next(); - if (generatorResult.done) { - done = true; - break; - } - const group = generatorResult.value; - currentGroups.push(group); - } - - if (done) { - const groupsTotal = groups.length + currentGroups.length; - setLastPage(Math.ceil(groupsTotal / pageSize)); - } - - setGroups((groups) => [...groups, ...currentGroups]); - }); - - // lastPage could be computed from groups.length and pageSize - const fetchInProgress = isLoading(groupsRequestState); - const canMoveForward = !fetchInProgress && (!lastPage || currentPage < lastPage); - // When going backward we already have the groups loaded, so no need to check if fetchInProgress - const canMoveBackward = currentPage > 1; - - const nextPage = useCallback(async () => { - if (canMoveForward) { - setCurrentPage((page) => page + 1); - } - }, [canMoveForward]); - - const previousPage = useCallback(async () => { - if (canMoveBackward) { - setCurrentPage((page) => page - 1); - } - }, [canMoveBackward]); - - // groups.length - pageSize to have one more page loaded to prevent flickering with loading state - // lastPage === undefined because 0 is falsy but a value which should stop fetching (e.g for broken data sources) - const shouldFetchNextPage = groups.length - pageSize < pageSize * currentPage && lastPage === undefined; - - if (shouldFetchNextPage && !fetchInProgress) { - fetchMoreGroups(pageSize); - } - - const groupsPage = useMemo(() => { - return groups.slice((currentPage - 1) * pageSize, currentPage * pageSize); - }, [groups, currentPage, pageSize]); - - return { isLoading: fetchInProgress, page: groupsPage, nextPage, previousPage, canMoveForward, canMoveBackward }; -} diff --git a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json index ed332ba2226..2e8360999c5 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json @@ -72,9 +72,9 @@ }, "config-editor": { "description-additional-settings": "Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy.", + "description-request-timeout": "Set the request timeout in seconds. Default is 30 seconds.", "title-additional-settings": "Additional settings", - "title-request-timeout": "Request timeout", - "description-request-timeout": "Set the request timeout in seconds. Default is 30 seconds." + "title-request-timeout": "Request Timeout" }, "current-user-fallback-credentials": { "alert-fallback-credentials-disabled": "Fallback credentials have been disabled. As user-based authentication only inherently supports requests with a user in scope, features such as alerting, recorded queries, or reporting will not function as expected. Please review the <2>documentation for more details.", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ed688e5c2d2..5b0f410a274 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1050,7 +1050,7 @@ "folder-bulk-actions": { "delete": { "button": { - "label": "Delete rules" + "label": "Delete all rules" } }, "delete-modal-confirmation-text": "Delete", @@ -1060,18 +1060,21 @@ "delete-modal-title": "Delete", "error": "Failed to execute action for folder: {{error}}", "more-button": { - "title": "Folder bulk Actions", - "tooltip": "Folder bulk Actions" + "title": "Folder actions", + "tooltip": "Folder actions" }, "pause": { "button": { - "label": "Pause all rule evaluation" + "label": "Pause all rules" } }, "unpause": { "button": { - "label": "Resume all rule evaluation" + "label": "Resume all rules" } + }, + "view": { + "folder": "View folder" } }, "folder-selector": { @@ -1206,15 +1209,6 @@ "grafana-rules-export-preview": { "text-loading": "Loading...." }, - "group-actions": { - "actions-trigger": "Rule group actions", - "details": "Details", - "edit": "Edit" - }, - "group-actions-menu": { - "group-load-failed": "Failed to load group details", - "unknown-error": "Unknown error" - }, "group-and-namespace-fields": { "group-picker-label-group": "Group", "namespace-picker-label-namespace": "Namespace" @@ -2070,8 +2064,7 @@ "new-datasource-recording-rule": "New Data source recording rule", "new-grafana-recording-rule": "New Grafana recording rule", "pagination": { - "next-page": "next page", - "previous-page": "previous page" + "next-page": "Show more…" }, "recording-rules": "Recording rules", "return-button": {