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
This commit is contained in:
+133
-73
@@ -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<boolean>(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 = (
|
||||
<>
|
||||
<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 && (
|
||||
<>
|
||||
{bulkActionsEnabled && <Menu.Divider />}
|
||||
<ExportFolderButton onClickExport={() => setIsExporting(true)} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown placement="bottom" overlay={<Menu>{menuItems}</Menu>}>
|
||||
<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}
|
||||
onDismiss={() => 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 (
|
||||
<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);
|
||||
@@ -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 (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
{canPause && (
|
||||
<>
|
||||
<PauseUnpauseActionMenuItem
|
||||
@@ -83,65 +198,10 @@ export const FolderBulkActionsButton = ({ folderUID }: Props) => {
|
||||
<Menu.Item
|
||||
label={t('alerting.folder-bulk-actions.delete.button.label', 'Delete all rules')}
|
||||
icon="trash-alt"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
disabled={deleteState.isLoading}
|
||||
onClick={() => onClickDelete(true)}
|
||||
disabled={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"
|
||||
/>
|
||||
)}
|
||||
</Dropdown>
|
||||
<DeleteModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onConfirm={onConfirmDelete}
|
||||
onDismiss={() => 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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user