RuleDetailsActionButtons.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import { css } from '@emotion/css';
  2. import React, { FC, Fragment, useState } from 'react';
  3. import { useDispatch } from 'react-redux';
  4. import { useLocation } from 'react-router-dom';
  5. import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
  6. import { config } from '@grafana/runtime';
  7. import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
  8. import { useAppNotification } from 'app/core/copy/appNotification';
  9. import { contextSrv } from 'app/core/services/context_srv';
  10. import { AccessControlAction } from 'app/types';
  11. import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
  12. import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
  13. import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
  14. import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
  15. import { deleteRuleAction } from '../../state/actions';
  16. import { getAlertmanagerByUid } from '../../utils/alertmanager';
  17. import { Annotation } from '../../utils/constants';
  18. import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
  19. import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
  20. import * as ruleId from '../../utils/rule-id';
  21. import { isFederatedRuleGroup } from '../../utils/rules';
  22. interface Props {
  23. rule: CombinedRule;
  24. rulesSource: RulesSource;
  25. }
  26. export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
  27. const dispatch = useDispatch();
  28. const location = useLocation();
  29. const notifyApp = useAppNotification();
  30. const style = useStyles2(getStyles);
  31. const { namespace, group, rulerRule } = rule;
  32. const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
  33. const alertId = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.id ?? '' : '';
  34. const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(alertId);
  35. const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
  36. ? rulesSource
  37. : getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
  38. const rulesSourceName = getRulesSourceName(rulesSource);
  39. const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
  40. const leftButtons: JSX.Element[] = [];
  41. const rightButtons: JSX.Element[] = [];
  42. const isFederated = isFederatedRuleGroup(group);
  43. const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
  44. const returnTo = location.pathname + location.search;
  45. const isViewMode = inViewMode(location.pathname);
  46. const deleteRule = () => {
  47. if (ruleToDelete && ruleToDelete.rulerRule) {
  48. const identifier = ruleId.fromRulerRule(
  49. getRulesSourceName(ruleToDelete.namespace.rulesSource),
  50. ruleToDelete.namespace.name,
  51. ruleToDelete.group.name,
  52. ruleToDelete.rulerRule
  53. );
  54. dispatch(deleteRuleAction(identifier, { navigateTo: isViewMode ? '/alerting/list' : undefined }));
  55. setRuleToDelete(undefined);
  56. }
  57. };
  58. const buildShareUrl = () => {
  59. if (isCloudRulesSource(rulesSource)) {
  60. const { appUrl, appSubUrl } = config;
  61. const baseUrl = appSubUrl !== '' ? `${appUrl}${appSubUrl}/` : config.appUrl;
  62. const ruleUrl = `${encodeURIComponent(rulesSource.name)}/${encodeURIComponent(rule.name)}`;
  63. return `${baseUrl}alerting/${ruleUrl}/find`;
  64. }
  65. return window.location.href.split('?')[0];
  66. };
  67. // explore does not support grafana rule queries atm
  68. // neither do "federated rules"
  69. if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
  70. leftButtons.push(
  71. <LinkButton
  72. className={style.button}
  73. size="xs"
  74. key="explore"
  75. variant="primary"
  76. icon="chart-line"
  77. target="__blank"
  78. href={createExploreLink(rulesSource.name, rule.query)}
  79. >
  80. See graph
  81. </LinkButton>
  82. );
  83. }
  84. if (rule.annotations[Annotation.runbookURL]) {
  85. leftButtons.push(
  86. <LinkButton
  87. className={style.button}
  88. size="xs"
  89. key="runbook"
  90. variant="primary"
  91. icon="book"
  92. target="__blank"
  93. href={textUtil.sanitizeUrl(rule.annotations[Annotation.runbookURL])}
  94. >
  95. View runbook
  96. </LinkButton>
  97. );
  98. }
  99. if (rule.annotations[Annotation.dashboardUID]) {
  100. const dashboardUID = rule.annotations[Annotation.dashboardUID];
  101. if (dashboardUID) {
  102. leftButtons.push(
  103. <LinkButton
  104. className={style.button}
  105. size="xs"
  106. key="dashboard"
  107. variant="primary"
  108. icon="apps"
  109. target="__blank"
  110. href={`d/${encodeURIComponent(dashboardUID)}`}
  111. >
  112. Go to dashboard
  113. </LinkButton>
  114. );
  115. const panelId = rule.annotations[Annotation.panelID];
  116. if (panelId) {
  117. leftButtons.push(
  118. <LinkButton
  119. className={style.button}
  120. size="xs"
  121. key="panel"
  122. variant="primary"
  123. icon="apps"
  124. target="__blank"
  125. href={`d/${encodeURIComponent(dashboardUID)}?viewPanel=${encodeURIComponent(panelId)}`}
  126. >
  127. Go to panel
  128. </LinkButton>
  129. );
  130. }
  131. }
  132. }
  133. if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
  134. leftButtons.push(
  135. <LinkButton
  136. className={style.button}
  137. size="xs"
  138. key="silence"
  139. icon="bell-slash"
  140. target="__blank"
  141. href={makeRuleBasedSilenceLink(alertmanagerSourceName, rule)}
  142. >
  143. Silence
  144. </LinkButton>
  145. );
  146. }
  147. if (alertId) {
  148. leftButtons.push(
  149. <Fragment key="history">
  150. <Button className={style.button} size="xs" icon="history" onClick={() => showStateHistoryModal()}>
  151. Show state history
  152. </Button>
  153. {StateHistoryModal}
  154. </Fragment>
  155. );
  156. }
  157. if (!isViewMode) {
  158. rightButtons.push(
  159. <LinkButton
  160. className={style.button}
  161. size="xs"
  162. key="view"
  163. variant="secondary"
  164. icon="eye"
  165. href={createViewLink(rulesSource, rule, returnTo)}
  166. >
  167. View
  168. </LinkButton>
  169. );
  170. }
  171. if (isEditable && rulerRule && !isFederated) {
  172. const sourceName = getRulesSourceName(rulesSource);
  173. const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
  174. const editURL = urlUtil.renderUrl(
  175. `${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
  176. {
  177. returnTo,
  178. }
  179. );
  180. if (isViewMode) {
  181. rightButtons.push(
  182. <ClipboardButton
  183. key="copy"
  184. onClipboardCopy={() => {
  185. notifyApp.success('URL copied!');
  186. }}
  187. onClipboardError={(copiedText) => {
  188. notifyApp.error('Error while copying URL', copiedText);
  189. }}
  190. className={style.button}
  191. size="sm"
  192. getText={buildShareUrl}
  193. >
  194. Copy link to rule
  195. </ClipboardButton>
  196. );
  197. }
  198. rightButtons.push(
  199. <LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
  200. Edit
  201. </LinkButton>
  202. );
  203. }
  204. if (isRemovable && rulerRule && !isFederated) {
  205. rightButtons.push(
  206. <Button
  207. className={style.button}
  208. size="xs"
  209. type="button"
  210. key="delete"
  211. variant="secondary"
  212. icon="trash-alt"
  213. onClick={() => setRuleToDelete(rule)}
  214. >
  215. Delete
  216. </Button>
  217. );
  218. }
  219. if (leftButtons.length || rightButtons.length) {
  220. return (
  221. <>
  222. <div className={style.wrapper}>
  223. <HorizontalGroup width="auto">{leftButtons.length ? leftButtons : <div />}</HorizontalGroup>
  224. <HorizontalGroup width="auto">{rightButtons.length ? rightButtons : <div />}</HorizontalGroup>
  225. </div>
  226. {!!ruleToDelete && (
  227. <ConfirmModal
  228. isOpen={true}
  229. title="Delete rule"
  230. body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
  231. confirmText="Yes, delete"
  232. icon="exclamation-triangle"
  233. onConfirm={deleteRule}
  234. onDismiss={() => setRuleToDelete(undefined)}
  235. />
  236. )}
  237. </>
  238. );
  239. }
  240. return null;
  241. };
  242. function inViewMode(pathname: string): boolean {
  243. return pathname.endsWith('/view');
  244. }
  245. export const getStyles = (theme: GrafanaTheme2) => ({
  246. wrapper: css`
  247. padding: ${theme.spacing(2)} 0;
  248. display: flex;
  249. flex-direction: row;
  250. justify-content: space-between;
  251. flex-wrap: wrap;
  252. border-bottom: solid 1px ${theme.colors.border.medium};
  253. `,
  254. button: css`
  255. height: 24px;
  256. margin-top: ${theme.spacing(1)};
  257. font-size: ${theme.typography.size.sm};
  258. `,
  259. });
  260. function isGrafanaRulerRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
  261. if (!rule) {
  262. return false;
  263. }
  264. return (rule as RulerGrafanaRuleDTO).grafana_alert != null;
  265. }