Alerting: Add export folder action to the new list view (#106256)

* Add export folder action to the new list view

* merge folder actions in one more button

* fix checking permissions

* remove commented code

* early return when no bulkactions are allowed

* fix css width

* address review comments

* move BulkActions component outside the parent component

* remove unnecessary check

* bring back accidentally removed code

* remove duplicated modal
pull/106286/head
Sonia Aguilar 2 months ago committed by GitHub
parent 8b262046e0
commit 5386b8ab09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 196
      public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx
  2. 21
      public/app/features/alerting/unified/components/rules/RulesGroup.tsx
  3. 19
      public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx
  4. 22
      public/locales/en-US/grafana.json

@ -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,33 +27,33 @@ 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<boolean>(false);
// abilities
const [pauseSupported, pauseAllowed] = useFolderBulkActionAbility(FolderBulkAction.Pause);
const [deleteSupported, deleteAllowed] = useFolderBulkActionAbility(FolderBulkAction.Delete);
// feature toggles
const bulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI;
const listView2Enabled = shouldUseAlertingListViewV2();
const canPause = pauseSupported && pauseAllowed;
const canDelete = deleteSupported && deleteAllowed;
const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const canExportRules = exportRulesSupported && exportRulesAllowed;
// 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 { folder } = useFolder(folderUID);
const folderName = folder?.title || 'unknown folder';
const folderUrl = makeFolderLink(folderUID);
const viewComponent = listView2Enabled ? 'list' : 'grouped';
// URLs
const redirectToListView = useRedirectToListView(viewComponent);
if (!canPause && !canDelete) {
if (!folder) {
return null;
}
@ -57,69 +64,33 @@ export const FolderBulkActionsButton = ({ folderUID }: Props) => {
const menuItems = (
<>
{canPause && (
<Menu.Item
url={folderUrl}
icon="eye"
aria-label={t('alerting.list-view.folder-actions.view.aria-label', 'View folder')}
label={t('alerting.list-view.folder-actions.view.label', 'View folder')}
/>
<BulkActions folderUID={folderUID} onClickDelete={setIsDeleteModalOpen} isLoading={deleteState.isLoading} />
{canExportRules && (
<>
<PauseUnpauseActionMenuItem
folderUID={folderUID}
action="pause"
executeAction={async (folderUID) => {
await pauseFolder({ namespace: folderUID }).unwrap();
await redirectToListView();
}}
isLoading={updateState.isLoading}
/>
<PauseUnpauseActionMenuItem
folderUID={folderUID}
action="unpause"
executeAction={async (folderUID) => {
await unpauseFolder({ namespace: folderUID }).unwrap();
await redirectToListView();
}}
isLoading={unpauseState.isLoading}
/>
{bulkActionsEnabled && <Menu.Divider />}
<ExportFolderButton onClickExport={() => setIsExporting(true)} />
</>
)}
{canDelete && (
<Menu.Item
label={t('alerting.folder-bulk-actions.delete.button.label', 'Delete all rules')}
icon="trash-alt"
onClick={() => setIsDeleteModalOpen(true)}
disabled={deleteState.isLoading}
/>
)}
{/* @TODO re-implement */}
{/* {listView2Enabled && (
<>
<Menu.Divider />
<Menu.Item
label={t('alerting.folder-bulk-actions.export.button.label', 'Export rules')}
icon="download-alt"
onClick={() => {}}
/>
</>
)} */}
</>
);
return (
<>
<Dropdown placement="bottom" overlay={<Menu>{menuItems}</Menu>}>
{listView2Enabled ? (
<MoreButton
fill="text"
size="sm"
aria-label={t('alerting.folder-bulk-actions.more-button.title', 'Folder actions')}
/>
) : (
<IconButton
name="ellipsis-h"
size="sm"
aria-label={t('alerting.folder-bulk-actions.more-button.title', 'Folder actions')}
tooltip={t('alerting.folder-bulk-actions.more-button.tooltip', 'Folder actions')}
tooltipPlacement="top"
/>
)}
<MoreButton
fill="text"
size="sm"
aria-label={t('alerting.list-view.folder-actions.button.aria-label', 'Folder actions')}
title={t('alerting.list-view.folder-actions.button.title', 'Actions')}
/>
</Dropdown>
{isExporting && <GrafanaRuleFolderExporter folder={folder} onClose={() => setIsExporting(false)} />}
<DeleteModal
isOpen={isDeleteModalOpen}
onConfirm={onConfirmDelete}
@ -145,3 +116,92 @@ function useRedirectToListView(view: string) {
return redirectToListView;
}
function ExportFolderButton({ onClickExport }: { onClickExport: () => void }) {
const { t } = useTranslate();
return (
<Menu.Item
aria-label={t('alerting.list-view.folder-actions.export.aria-label', 'Export rules folder')}
data-testid="export-folder"
key="export-folder"
label={t('alerting.list-view.folder-actions.export.label', 'Export rules folder')}
icon="download-alt"
onClick={onClickExport}
/>
);
}
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);
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();
// URLs
const viewComponent = listView2Enabled ? 'list' : 'grouped';
const redirectToListView = useRedirectToListView(viewComponent);
if (!bulkActionsEnabled) {
return null;
}
if (!canPause && !canDelete) {
return null;
}
return (
<>
<Menu.Divider />
{canPause && (
<>
<PauseUnpauseActionMenuItem
folderUID={folderUID}
action="pause"
executeAction={async (folderUID) => {
await pauseFolder({ namespace: folderUID }).unwrap();
await redirectToListView();
}}
isLoading={updateState.isLoading}
/>
<PauseUnpauseActionMenuItem
folderUID={folderUID}
action="unpause"
executeAction={async (folderUID) => {
await unpauseFolder({ namespace: folderUID }).unwrap();
await redirectToListView();
}}
isLoading={unpauseState.isLoading}
/>
</>
)}
{canDelete && (
<Menu.Item
label={t('alerting.folder-bulk-actions.delete.button.label', 'Delete all rules')}
icon="trash-alt"
onClick={() => onClickDelete(true)}
disabled={isLoading}
/>
)}
</>
);
}

@ -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(
<ActionIcon
aria-label={t('alerting.rule-group-action.export-rules-folder', 'Export rules folder')}
data-testid="export-folder"
key="export-folder"
icon="download-alt"
tooltip={t('alerting.rule-group-action.export-rules-folder', 'Export rules folder')}
onClick={() => setIsExporting('folder')}
/>
);
if (isFolderBulkActionsEnabled && folderUID && isListView) {
actionIcons.push(<FolderBulkActionsButton folderUID={folderUID} key="folder-bulk-actions" />);
}
actionIcons.push(<FolderActionsButton folderUID={folderUID} key="folder-bulk-actions" />);
}
}
}
@ -333,7 +318,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
margin: `0 ${theme.spacing(2)}`,
}),
actionIcons: css({
width: '80px',
width: '120px',
alignItems: 'center',
flexShrink: 0,

@ -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 (
<DataSourceSection name="Grafana" application="grafana" uid={GrafanaRulesSourceSymbol} isLoading={isLoading}>
<Stack direction="column" gap={0}>
{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 (
<ListSection
@ -65,14 +59,7 @@ export function PaginatedGrafanaLoader() {
</Text>
</Stack>
}
actions={
<>
<LinkButton variant="secondary" fill="text" size="sm" href={folderUrl}>
<Trans i18nKey="alerting.folder-bulk-actions.view.folder">View folder</Trans>
</LinkButton>
{isFolderBulkActionsEnabled ? <FolderBulkActionsButton folderUID={folderUid} /> : null}
</>
}
actions={<FolderActionsButton folderUID={folderUid} />}
>
{groups.map((group) => (
<GrafanaRuleGroupListItem

@ -1197,10 +1197,6 @@
"delete-modal-text": "This action will delete all alert rules in the <1>{{folderName}}</1> 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"
},

Loading…
Cancel
Save