Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
alerting
/
unified
/
components
/
rules
/
File Content:
RulesGroup.tsx
import { css } from '@emotion/css'; import pluralize from 'pluralize'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; 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( <HorizontalGroup key="is-deleting"> <Spinner /> deleting </HorizontalGroup> ); } else if (rulesSource === GRAFANA_RULES_SOURCE_NAME) { if (folderUID) { const baseUrl = makeFolderLink(folderUID); if (folder?.canSave) { if (isGroupView && !isProvisioned) { actionIcons.push( <ActionIcon aria-label="edit rule group" data-testid="edit-group" key="edit" icon="pen" tooltip="edit rule group" onClick={() => setIsEditingGroup(true)} /> ); actionIcons.push( <ActionIcon aria-label="re-order rules" data-testid="reorder-group" key="reorder" icon="exchange-alt" tooltip="reorder rules" className={styles.rotate90} onClick={() => setIsReorderingGroup(true)} /> ); } if (isListView) { actionIcons.push( <ActionIcon aria-label="go to folder" key="goto" icon="folder-open" tooltip="go to folder" to={baseUrl} target="__blank" /> ); if (folder?.canAdmin) { actionIcons.push( <ActionIcon aria-label="manage permissions" key="manage-perms" icon="lock" tooltip="manage permissions" to={baseUrl + '/permissions'} target="__blank" /> ); } } } if (folder) { if (isListView) { actionIcons.push( <ActionIcon aria-label="export rule folder" data-testid="export-folder" key="export-folder" icon="download-alt" tooltip="Export rules folder" onClick={() => setIsExporting('folder')} /> ); } else if (isGroupView) { actionIcons.push( <ActionIcon aria-label="export rule group" data-testid="export-group" key="export-group" icon="download-alt" tooltip="Export rule group" onClick={() => setIsExporting('group')} /> ); } } } } else if (canEditRules(rulesSource.name) && hasRuler(rulesSource)) { if (!isFederated) { actionIcons.push( <ActionIcon aria-label="edit rule group" data-testid="edit-group" key="edit" icon="pen" tooltip="edit rule group" onClick={() => setIsEditingGroup(true)} /> ); actionIcons.push( <ActionIcon aria-label="re-order rules" data-testid="reorder-group" key="reorder" icon="exchange-alt" tooltip="re-order rules" className={styles.rotate90} onClick={() => setIsReorderingGroup(true)} /> ); } actionIcons.push( <ActionIcon aria-label="delete rule group" data-testid="delete-group" key="delete-group" icon="trash-alt" tooltip="delete rule group" onClick={() => setIsDeletingGroup(true)} /> ); } // ungrouped rules are rules that are in the "default" group name const groupName = isListView ? ( <RuleLocation namespace={decodeGrafanaNamespace(namespace).name} /> ) : ( <RuleLocation namespace={decodeGrafanaNamespace(namespace).name} group={group.name} /> ); const closeEditModal = (saved = false) => { if (!saved) { logInfo(LogMessages.leavingRuleGroupEdit); } setIsEditingGroup(false); }; return ( <div className={styles.wrapper} data-testid="rule-group"> <div className={styles.header} data-testid="rule-group-header"> <CollapseToggle size="sm" className={styles.collapseToggle} isCollapsed={isCollapsed} onToggle={setIsCollapsed} data-testid="group-collapse-toggle" /> <Icon name={isCollapsed ? 'folder' : 'folder-open'} /> {isCloudRulesSource(rulesSource) && ( <Tooltip content={rulesSource.name} placement="top"> <img alt={rulesSource.meta.name} className={styles.dataSourceIcon} src={rulesSource.meta.info.logos.small} /> </Tooltip> )} { // eslint-disable-next-line <div className={styles.groupName} onClick={() => setIsCollapsed(!isCollapsed)}> {isFederated && <Badge color="purple" text="Federated" />} {groupName} </div> } <div className={styles.spacer} /> <div className={styles.headerStats}> <RuleGroupStats group={group} /> </div> {isProvisioned && ( <> <div className={styles.actionsSeparator}>|</div> <div className={styles.actionIcons}> <Badge color="purple" text="Provisioned" /> </div> </> )} {!!actionIcons.length && ( <> <div className={styles.actionsSeparator}>|</div> <div className={styles.actionIcons}> <Stack gap={0.5}>{actionIcons}</Stack> </div> </> )} </div> {!isCollapsed && ( <RulesTable showSummaryColumn={true} className={styles.rulesTable} showGuidelines={true} showNextEvaluationColumn={Boolean(group.interval)} rules={group.rules} /> )} {isEditingGroup && ( <EditCloudGroupModal namespace={namespace} group={group} onClose={() => closeEditModal()} folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder.uid) : undefined} folderUid={folderUID} /> )} {isReorderingGroup && ( <ReorderCloudGroupModal group={group} folderUid={folderUID} namespace={namespace} onClose={() => setIsReorderingGroup(false)} /> )} <ConfirmModal isOpen={isDeletingGroup} title="Delete group" body={ <div> <p> Deleting "<strong>{group.name}</strong>" will permanently remove the group and{' '} {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it. </p> <p>Are you sure you want to delete this group?</p> </div> } onConfirm={deleteGroup} onDismiss={() => setIsDeletingGroup(false)} confirmText="Delete" /> {folder && isExporting === 'folder' && ( <GrafanaRuleFolderExporter folder={folder} onClose={() => setIsExporting(undefined)} /> )} {folder && isExporting === 'group' && ( <GrafanaRuleGroupExporter folderUid={folder.uid} groupName={group.name} onClose={() => setIsExporting(undefined)} /> )} </div> ); }); 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); `, }; };
Submit
FILE
FOLDER
INFO
Name
Size
Permission
Action
state-history
---
0755
ActionButton.tsx
656 bytes
0644
ActionIcon.tsx
1221 bytes
0644
AlertInstanceDetails.tsx
890 bytes
0644
AlertInstanceStateFilter.tsx
2279 bytes
0644
AlertInstancesTable.tsx
3137 bytes
0644
AlertStateTag.tsx
798 bytes
0644
CloneRule.tsx
2736 bytes
0644
CloudRules.tsx
4731 bytes
0644
EditRuleGroupModal.test.tsx
6785 bytes
0644
EditRuleGroupModal.tsx
13135 bytes
0644
GrafanaRules.tsx
4432 bytes
0644
MultipleDataSourcePicker.tsx
5591 bytes
0644
NoRulesCTA.tsx
1878 bytes
0644
ReorderRuleGroupModal.test.tsx
341 bytes
0644
ReorderRuleGroupModal.tsx
6809 bytes
0644
RuleActionsButtons.tsx
7169 bytes
0644
RuleConfigStatus.tsx
1410 bytes
0644
RuleDetails.test.tsx
5891 bytes
0644
RuleDetails.tsx
4376 bytes
0644
RuleDetailsActionButtons.tsx
11094 bytes
0644
RuleDetailsAnnotations.tsx
880 bytes
0644
RuleDetailsDataSources.tsx
2159 bytes
0644
RuleDetailsExpression.tsx
927 bytes
0644
RuleDetailsFederatedSources.tsx
541 bytes
0644
RuleDetailsMatchingInstances.test.tsx
6494 bytes
0644
RuleDetailsMatchingInstances.tsx
6532 bytes
0644
RuleHealth.tsx
949 bytes
0644
RuleListErrors.tsx
5247 bytes
0644
RuleListGroupView.test.tsx
4198 bytes
0644
RuleListGroupView.tsx
1485 bytes
0644
RuleListStateSection.tsx
1413 bytes
0644
RuleListStateView.tsx
2178 bytes
0644
RuleState.tsx
2363 bytes
0644
RuleStats.tsx
3651 bytes
0644
RulesFilter.test.tsx
2591 bytes
0644
RulesFilter.tsx
10765 bytes
0644
RulesGroup.test.tsx
7427 bytes
0644
RulesGroup.tsx
12418 bytes
0644
RulesTable.test.tsx
4684 bytes
0644
RulesTable.tsx
8979 bytes
0644
useCombinedGroupNamespace.tsx
378 bytes
0644
N4ST4R_ID | Naxtarrr