import { css } from '@emotion/css'; import pluralize from 'pluralize'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; import { LogMessages, logInfo } from '../../Analytics'; import { useFolder } from '../../hooks/useFolder'; import { useHasRuler } from '../../hooks/useHasRuler'; import { deleteRulesGroupAction } from '../../state/actions'; import { useRulesAccess } from '../../utils/accessControlHooks'; import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { makeFolderLink, makeFolderSettingsLink } from '../../utils/misc'; import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; import { CollapseToggle } from '../CollapseToggle'; import { RuleLocation } from '../RuleLocation'; import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter'; import { GrafanaRuleGroupExporter } from '../export/GrafanaRuleGroupExporter'; import { decodeGrafanaNamespace } from '../expressions/util'; import { ActionIcon } from './ActionIcon'; import { EditCloudGroupModal } from './EditRuleGroupModal'; import { ReorderCloudGroupModal } from './ReorderRuleGroupModal'; import { RuleGroupStats } from './RuleStats'; import { RulesTable } from './RulesTable'; type ViewMode = 'grouped' | 'list'; interface Props { namespace: CombinedRuleNamespace; group: CombinedRuleGroup; expandAll: boolean; viewMode: ViewMode; } export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => { const { rulesSource } = namespace; const dispatch = useDispatch(); const styles = useStyles2(getStyles); const [isEditingGroup, setIsEditingGroup] = useState(false); const [isDeletingGroup, setIsDeletingGroup] = useState(false); const [isReorderingGroup, setIsReorderingGroup] = useState(false); const [isExporting, setIsExporting] = useState<'group' | 'folder' | undefined>(undefined); const [isCollapsed, setIsCollapsed] = useState(!expandAll); const { canEditRules } = useRulesAccess(); useEffect(() => { setIsCollapsed(!expandAll); }, [expandAll]); const { hasRuler, rulerRulesLoaded } = useHasRuler(); const rulerRule = group.rules[0]?.rulerRule; const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined; const { folder } = useFolder(folderUID); // group "is deleting" if rules source has ruler, but this group has no rules that are in ruler const isDeleting = hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule); const isFederated = isFederatedRuleGroup(group); // check if group has provisioned items const isProvisioned = group.rules.some((rule) => { return isGrafanaRulerRule(rule.rulerRule) && rule.rulerRule.grafana_alert.provenance; }); // check what view mode we are in const isListView = viewMode === 'list'; const isGroupView = viewMode === 'grouped'; const deleteGroup = () => { dispatch(deleteRulesGroupAction(namespace, group)); setIsDeletingGroup(false); }; const actionIcons: React.ReactNode[] = []; // for grafana, link to folder views if (isDeleting) { actionIcons.push( deleting ); } else if (rulesSource === GRAFANA_RULES_SOURCE_NAME) { if (folderUID) { const baseUrl = makeFolderLink(folderUID); if (folder?.canSave) { if (isGroupView && !isProvisioned) { actionIcons.push( setIsEditingGroup(true)} /> ); actionIcons.push( setIsReorderingGroup(true)} /> ); } if (isListView) { actionIcons.push( ); if (folder?.canAdmin) { actionIcons.push( ); } } } if (folder) { if (isListView) { actionIcons.push( setIsExporting('folder')} /> ); } else if (isGroupView) { actionIcons.push( setIsExporting('group')} /> ); } } } } else if (canEditRules(rulesSource.name) && hasRuler(rulesSource)) { if (!isFederated) { actionIcons.push( setIsEditingGroup(true)} /> ); actionIcons.push( setIsReorderingGroup(true)} /> ); } actionIcons.push( setIsDeletingGroup(true)} /> ); } // ungrouped rules are rules that are in the "default" group name const groupName = isListView ? ( ) : ( ); const closeEditModal = (saved = false) => { if (!saved) { logInfo(LogMessages.leavingRuleGroupEdit); } setIsEditingGroup(false); }; return (
{isCloudRulesSource(rulesSource) && ( {rulesSource.meta.name} )} { // eslint-disable-next-line
setIsCollapsed(!isCollapsed)}> {isFederated && } {groupName}
}
{isProvisioned && ( <>
|
)} {!!actionIcons.length && ( <>
|
{actionIcons}
)}
{!isCollapsed && ( )} {isEditingGroup && ( closeEditModal()} folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder.uid) : undefined} folderUid={folderUID} /> )} {isReorderingGroup && ( setIsReorderingGroup(false)} /> )}

Deleting "{group.name}" will permanently remove the group and{' '} {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it.

Are you sure you want to delete this group?

} onConfirm={deleteGroup} onDismiss={() => setIsDeletingGroup(false)} confirmText="Delete" /> {folder && isExporting === 'folder' && ( setIsExporting(undefined)} /> )} {folder && isExporting === 'group' && ( setIsExporting(undefined)} /> )}
); }); RulesGroup.displayName = 'RulesGroup'; export const getStyles = (theme: GrafanaTheme2) => { return { wrapper: css``, header: css` display: flex; flex-direction: row; align-items: center; padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0; flex-wrap: nowrap; border-bottom: 1px solid ${theme.colors.border.weak}; &:hover { background-color: ${theme.components.table.rowHoverBackground}; } `, headerStats: css` flex-shrink: 0; span { vertical-align: middle; } ${theme.breakpoints.down('sm')} { order: 2; width: 100%; padding-left: ${theme.spacing(1)}; } `, groupName: css` margin-left: ${theme.spacing(1)}; margin-bottom: 0; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `, spacer: css` flex: 1; `, collapseToggle: css` background: none; border: none; margin-top: -${theme.spacing(1)}; margin-bottom: -${theme.spacing(1)}; svg { margin-bottom: 0; } `, dataSourceIcon: css` width: ${theme.spacing(2)}; height: ${theme.spacing(2)}; margin-left: ${theme.spacing(2)}; `, dataSourceOrigin: css` margin-right: 1em; color: ${theme.colors.text.disabled}; `, actionsSeparator: css` margin: 0 ${theme.spacing(2)}; `, actionIcons: css` width: 80px; align-items: center; flex-shrink: 0; `, rulesTable: css` margin: ${theme.spacing(2, 0)}; `, rotate90: css` transform: rotate(90deg); `, }; };