RulesGroup.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { css } from '@emotion/css';
  2. import pluralize from 'pluralize';
  3. import React, { FC, useEffect, useState } from 'react';
  4. import { useDispatch } from 'react-redux';
  5. import { GrafanaTheme2 } from '@grafana/data';
  6. import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
  7. import kbn from 'app/core/utils/kbn';
  8. import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
  9. import { useFolder } from '../../hooks/useFolder';
  10. import { useHasRuler } from '../../hooks/useHasRuler';
  11. import { deleteRulesGroupAction } from '../../state/actions';
  12. import { useRulesAccess } from '../../utils/accessControlHooks';
  13. import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource';
  14. import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
  15. import { CollapseToggle } from '../CollapseToggle';
  16. import { RuleLocation } from '../RuleLocation';
  17. import { ActionIcon } from './ActionIcon';
  18. import { EditCloudGroupModal } from './EditCloudGroupModal';
  19. import { RuleStats } from './RuleStats';
  20. import { RulesTable } from './RulesTable';
  21. interface Props {
  22. namespace: CombinedRuleNamespace;
  23. group: CombinedRuleGroup;
  24. expandAll: boolean;
  25. }
  26. export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }) => {
  27. const { rulesSource } = namespace;
  28. const dispatch = useDispatch();
  29. const styles = useStyles2(getStyles);
  30. const [isEditingGroup, setIsEditingGroup] = useState(false);
  31. const [isDeletingGroup, setIsDeletingGroup] = useState(false);
  32. const [isCollapsed, setIsCollapsed] = useState(!expandAll);
  33. const { canEditRules } = useRulesAccess();
  34. useEffect(() => {
  35. setIsCollapsed(!expandAll);
  36. }, [expandAll]);
  37. const { hasRuler, rulerRulesLoaded } = useHasRuler();
  38. const rulerRule = group.rules[0]?.rulerRule;
  39. const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined;
  40. const { folder } = useFolder(folderUID);
  41. // group "is deleting" if rules source has ruler, but this group has no rules that are in ruler
  42. const isDeleting =
  43. hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule);
  44. const isFederated = isFederatedRuleGroup(group);
  45. const deleteGroup = () => {
  46. dispatch(deleteRulesGroupAction(namespace, group));
  47. setIsDeletingGroup(false);
  48. };
  49. const actionIcons: React.ReactNode[] = [];
  50. // for grafana, link to folder views
  51. if (isDeleting) {
  52. actionIcons.push(
  53. <HorizontalGroup key="is-deleting">
  54. <Spinner />
  55. deleting
  56. </HorizontalGroup>
  57. );
  58. } else if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
  59. if (folderUID) {
  60. const baseUrl = `/dashboards/f/${folderUID}/${kbn.slugifyForUrl(namespace.name)}`;
  61. if (folder?.canSave) {
  62. actionIcons.push(
  63. <ActionIcon
  64. aria-label="edit folder"
  65. key="edit"
  66. icon="pen"
  67. tooltip="edit folder"
  68. to={baseUrl + '/settings'}
  69. target="__blank"
  70. />
  71. );
  72. }
  73. if (folder?.canAdmin) {
  74. actionIcons.push(
  75. <ActionIcon
  76. aria-label="manage permissions"
  77. key="manage-perms"
  78. icon="lock"
  79. tooltip="manage permissions"
  80. to={baseUrl + '/permissions'}
  81. target="__blank"
  82. />
  83. );
  84. }
  85. }
  86. } else if (canEditRules(rulesSource.name) && hasRuler(rulesSource)) {
  87. if (!isFederated) {
  88. actionIcons.push(
  89. <ActionIcon
  90. aria-label="edit rule group"
  91. data-testid="edit-group"
  92. key="edit"
  93. icon="pen"
  94. tooltip="edit rule group"
  95. onClick={() => setIsEditingGroup(true)}
  96. />
  97. );
  98. }
  99. actionIcons.push(
  100. <ActionIcon
  101. aria-label="delete rule group"
  102. data-testid="delete-group"
  103. key="delete-group"
  104. icon="trash-alt"
  105. tooltip="delete rule group"
  106. onClick={() => setIsDeletingGroup(true)}
  107. />
  108. );
  109. }
  110. // ungrouped rules are rules that are in the "default" group name
  111. const isUngrouped = group.name === 'default';
  112. const groupName = isUngrouped ? (
  113. <RuleLocation namespace={namespace.name} />
  114. ) : (
  115. <RuleLocation namespace={namespace.name} group={group.name} />
  116. );
  117. return (
  118. <div className={styles.wrapper} data-testid="rule-group">
  119. <div className={styles.header} data-testid="rule-group-header">
  120. <CollapseToggle
  121. className={styles.collapseToggle}
  122. isCollapsed={isCollapsed}
  123. onToggle={setIsCollapsed}
  124. data-testid="group-collapse-toggle"
  125. />
  126. <Icon name={isCollapsed ? 'folder' : 'folder-open'} />
  127. {isCloudRulesSource(rulesSource) && (
  128. <Tooltip content={rulesSource.name} placement="top">
  129. <img
  130. alt={rulesSource.meta.name}
  131. className={styles.dataSourceIcon}
  132. src={rulesSource.meta.info.logos.small}
  133. />
  134. </Tooltip>
  135. )}
  136. <h6 className={styles.heading}>
  137. {isFederated && <Badge color="purple" text="Federated" />} {groupName}
  138. </h6>
  139. <div className={styles.spacer} />
  140. <div className={styles.headerStats}>
  141. <RuleStats showInactive={false} group={group} />
  142. </div>
  143. {!!actionIcons.length && (
  144. <>
  145. <div className={styles.actionsSeparator}>|</div>
  146. <div className={styles.actionIcons}>{actionIcons}</div>
  147. </>
  148. )}
  149. </div>
  150. {!isCollapsed && (
  151. <RulesTable showSummaryColumn={true} className={styles.rulesTable} showGuidelines={true} rules={group.rules} />
  152. )}
  153. {isEditingGroup && (
  154. <EditCloudGroupModal group={group} namespace={namespace} onClose={() => setIsEditingGroup(false)} />
  155. )}
  156. <ConfirmModal
  157. isOpen={isDeletingGroup}
  158. title="Delete group"
  159. body={
  160. <div>
  161. Deleting this group will permanently remove the group
  162. <br />
  163. and {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it.
  164. <br />
  165. Are you sure you want to delete this group?
  166. </div>
  167. }
  168. onConfirm={deleteGroup}
  169. onDismiss={() => setIsDeletingGroup(false)}
  170. confirmText="Delete"
  171. />
  172. </div>
  173. );
  174. });
  175. RulesGroup.displayName = 'RulesGroup';
  176. export const getStyles = (theme: GrafanaTheme2) => ({
  177. wrapper: css`
  178. & + & {
  179. margin-top: ${theme.spacing(2)};
  180. }
  181. `,
  182. header: css`
  183. display: flex;
  184. flex-direction: row;
  185. align-items: center;
  186. padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0;
  187. background-color: ${theme.colors.background.secondary};
  188. flex-wrap: wrap;
  189. `,
  190. headerStats: css`
  191. span {
  192. vertical-align: middle;
  193. }
  194. ${theme.breakpoints.down('sm')} {
  195. order: 2;
  196. width: 100%;
  197. padding-left: ${theme.spacing(1)};
  198. }
  199. `,
  200. heading: css`
  201. margin-left: ${theme.spacing(1)};
  202. margin-bottom: 0;
  203. `,
  204. spacer: css`
  205. flex: 1;
  206. `,
  207. collapseToggle: css`
  208. background: none;
  209. border: none;
  210. margin-top: -${theme.spacing(1)};
  211. margin-bottom: -${theme.spacing(1)};
  212. svg {
  213. margin-bottom: 0;
  214. }
  215. `,
  216. dataSourceIcon: css`
  217. width: ${theme.spacing(2)};
  218. height: ${theme.spacing(2)};
  219. margin-left: ${theme.spacing(2)};
  220. `,
  221. dataSourceOrigin: css`
  222. margin-right: 1em;
  223. color: ${theme.colors.text.disabled};
  224. `,
  225. actionsSeparator: css`
  226. margin: 0 ${theme.spacing(2)};
  227. `,
  228. actionIcons: css`
  229. & > * + * {
  230. margin-left: ${theme.spacing(0.5)};
  231. }
  232. `,
  233. rulesTable: css`
  234. margin-top: ${theme.spacing(3)};
  235. `,
  236. });