diff --git a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx index 373ae7a91d3..0394db180ce 100644 --- a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx +++ b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx @@ -2,17 +2,24 @@ import { useState } from 'react'; import { useTranslate } from '@grafana/i18n'; import { config, locationService } from '@grafana/runtime'; -import { Dropdown, IconButton, Menu } from '@grafana/ui'; +import { Dropdown, Menu } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { alertingFolderActionsApi } from '../../api/alertingFolderActionsApi'; -import { shouldUsePrometheusRulesPrimary } from '../../featureToggles'; -import { FolderBulkAction, useFolderBulkActionAbility } from '../../hooks/useAbilities'; +import { shouldUseAlertingListViewV2, shouldUsePrometheusRulesPrimary } from '../../featureToggles'; +import { + AlertingAction, + FolderBulkAction, + useAlertingAbility, + useFolderBulkActionAbility, +} from '../../hooks/useAbilities'; import { useFolder } from '../../hooks/useFolder'; import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../../state/actions'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { makeFolderLink } from '../../utils/misc'; import { createRelativeUrl } from '../../utils/url'; import MoreButton from '../MoreButton'; +import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter'; import { DeleteModal } from './DeleteModal'; import { PauseUnpauseActionMenuItem } from './PauseUnpauseActionMenuItem'; @@ -20,11 +27,124 @@ interface Props { folderUID: string; } -export const FolderBulkActionsButton = ({ folderUID }: Props) => { +export const FolderActionsButton = ({ folderUID }: Props) => { const { t } = useTranslate(); // state const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isExporting, setIsExporting] = useState(false); + + // feature toggles + const bulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; + const listView2Enabled = shouldUseAlertingListViewV2(); + + const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); + + const canExportRules = exportRulesSupported && exportRulesAllowed; + + const [deleteGrafanaRulesFromFolder, deleteState] = + alertingFolderActionsApi.endpoints.deleteGrafanaRulesFromFolder.useMutation(); + + const { folder } = useFolder(folderUID); + const folderName = folder?.title || 'unknown folder'; + const folderUrl = makeFolderLink(folderUID); + const viewComponent = listView2Enabled ? 'list' : 'grouped'; + + // URLs + const redirectToListView = useRedirectToListView(viewComponent); + + if (!folder) { + return null; + } + + const onConfirmDelete = async () => { + await deleteGrafanaRulesFromFolder({ namespace: folderUID }).unwrap(); + await redirectToListView(); + }; + + const menuItems = ( + <> + + + {canExportRules && ( + <> + {bulkActionsEnabled && } + setIsExporting(true)} /> + + )} + + ); + + return ( + <> + {menuItems}}> + + + {isExporting && setIsExporting(false)} />} + setIsDeleteModalOpen(false)} + folderName={folderName} + /> + + ); +}; + +function useRedirectToListView(view: string) { + const dispatch = useDispatch(); + const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); + const redirectToListView = async () => { + if (prometheusRulesPrimary) { + await dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME })); + await dispatch(fetchAllPromRulesAction(false)); + } else { + await dispatch(fetchAllPromAndRulerRulesAction(false)); + } + locationService.push(createRelativeUrl('/alerting/list', { view }, { skipSubPath: true })); + }; + + return redirectToListView; +} + +function ExportFolderButton({ onClickExport }: { onClickExport: () => void }) { + const { t } = useTranslate(); + return ( + + ); +} + +function BulkActions({ + folderUID, + onClickDelete, + isLoading, +}: { + folderUID: string; + onClickDelete: (showModal: boolean) => void; + isLoading: boolean; +}) { + const { t } = useTranslate(); + + // feature toggles + const listView2Enabled = shouldUseAlertingListViewV2(); + const bulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; // abilities const [pauseSupported, pauseAllowed] = useFolderBulkActionAbility(FolderBulkAction.Pause); @@ -36,27 +156,22 @@ export const FolderBulkActionsButton = ({ folderUID }: Props) => { // mutations const [pauseFolder, updateState] = alertingFolderActionsApi.endpoints.pauseFolder.useMutation(); const [unpauseFolder, unpauseState] = alertingFolderActionsApi.endpoints.unpauseFolder.useMutation(); - const [deleteGrafanaRulesFromFolder, deleteState] = - alertingFolderActionsApi.endpoints.deleteGrafanaRulesFromFolder.useMutation(); - - const folderName = useFolder(folderUID).folder?.title || 'unknown folder'; - const listView2Enabled = config.featureToggles.alertingListViewV2 ?? false; - const viewComponent = listView2Enabled ? 'list' : 'grouped'; // URLs + const viewComponent = listView2Enabled ? 'list' : 'grouped'; const redirectToListView = useRedirectToListView(viewComponent); + if (!bulkActionsEnabled) { + return null; + } + if (!canPause && !canDelete) { return null; } - const onConfirmDelete = async () => { - await deleteGrafanaRulesFromFolder({ namespace: folderUID }).unwrap(); - await redirectToListView(); - }; - - const menuItems = ( + return ( <> + {canPause && ( <> { setIsDeleteModalOpen(true)} - disabled={deleteState.isLoading} + onClick={() => onClickDelete(true)} + disabled={isLoading} /> )} - {/* @TODO re-implement */} - {/* {listView2Enabled && ( - <> - - {}} - /> - - )} */} ); - - return ( - <> - {menuItems}}> - {listView2Enabled ? ( - - ) : ( - - )} - - setIsDeleteModalOpen(false)} - folderName={folderName} - /> - - ); -}; - -function useRedirectToListView(view: string) { - const dispatch = useDispatch(); - const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); - const redirectToListView = async () => { - if (prometheusRulesPrimary) { - await dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME })); - await dispatch(fetchAllPromRulesAction(false)); - } else { - await dispatch(fetchAllPromAndRulerRulesAction(false)); - } - locationService.push(createRelativeUrl('/alerting/list', { view }, { skipSubPath: true })); - }; - - return redirectToListView; } diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index 3bf79f45c0e..406111a7afd 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, useTranslate } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; import { Badge, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { CombinedRuleGroup, CombinedRuleNamespace, RulesSource } from 'app/types/unified-alerting'; @@ -19,7 +18,7 @@ import { CollapseToggle } from '../CollapseToggle'; import { RuleLocation } from '../RuleLocation'; import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter'; import { decodeGrafanaNamespace } from '../expressions/util'; -import { FolderBulkActionsButton } from '../folder-actions/FolderActionsButton'; +import { FolderActionsButton } from '../folder-actions/FolderActionsButton'; import { ActionIcon } from './ActionIcon'; import { RuleGroupStats } from './RuleStats'; @@ -69,8 +68,6 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: const canEditGroup = hasRuler && !isProvisioned && !isFederated && !isPluginProvided && canEditRules(rulesSourceName); - const isFolderBulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; - // check what view mode we are in const isListView = viewMode === 'list'; const isGroupView = viewMode === 'grouped'; @@ -139,19 +136,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: } if (folder) { if (isListView) { - actionIcons.push( - setIsExporting('folder')} - /> - ); - if (isFolderBulkActionsEnabled && folderUID && isListView) { - actionIcons.push(); - } + actionIcons.push(); } } } @@ -333,7 +318,7 @@ export const getStyles = (theme: GrafanaTheme2) => { margin: `0 ${theme.spacing(2)}`, }), actionIcons: css({ - width: '80px', + width: '120px', alignItems: 'center', flexShrink: 0, diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index 43a81389079..cb728adb63b 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -1,16 +1,13 @@ import { groupBy, isEmpty } from 'lodash'; import { useEffect, useMemo, useRef } from 'react'; -import { Trans } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; -import { Icon, LinkButton, Stack, Text } from '@grafana/ui'; +import { Icon, 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-actions/FolderActionsButton'; +import { FolderActionsButton } from '../components/folder-actions/FolderActionsButton'; import { GrafanaNoRulesCTA } from '../components/rules/NoRulesCTA'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -import { makeFolderLink } from '../utils/misc'; import { groups } from '../utils/navigation'; import { GrafanaGroupLoader } from './GrafanaGroupLoader'; @@ -44,15 +41,12 @@ export function PaginatedGrafanaLoader() { const groupsByFolder = useMemo(() => groupBy(groups, 'folderUid'), [groups]); const hasNoRules = isEmpty(groups) && !isLoading; - 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={ - <> - - View folder - - {isFolderBulkActionsEnabled ? : null} - - } + actions={} > {groups.map((group) => ( {{folderName}} folder. Nested folders will not be affected.", "delete-modal-title": "Delete", "error": "Failed to execute action for folder: {{error}}", - "more-button": { - "title": "Folder actions", - "tooltip": "Folder actions" - }, "pause": { "button": { "label": "Pause all rules" @@ -1210,9 +1206,6 @@ "button": { "label": "Resume all rules" } - }, - "view": { - "folder": "View folder" } }, "folder-selector": { @@ -1664,6 +1657,20 @@ "no-rules-created": "You haven't created any rules yet", "provisioning": "You can also define rules through file provisioning or Terraform" }, + "folder-actions": { + "button": { + "aria-label": "Folder actions", + "title": "Actions" + }, + "export": { + "aria-label": "Export rules folder", + "label": "Export rules folder" + }, + "view": { + "aria-label": "View folder", + "label": "View folder" + } + }, "no-prom-or-loki-rules": "There are no Prometheus or Loki data sources configured", "no-rules": "No rules found.", "section": { @@ -2329,7 +2336,6 @@ "rule-group-action": { "details": "rule group details", "edit": "edit rule group", - "export-rules-folder": "Export rules folder", "go-to-folder": "go to folder", "manage-permissions": "manage permissions" },