rule.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import {Fragment} from 'react';
  2. import {DraggableSyntheticListeners, UseDraggableArguments} from '@dnd-kit/core';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import MenuItemActionLink from 'sentry/components/actions/menuItemActionLink';
  6. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  7. import Button from 'sentry/components/button';
  8. import DropdownLink from 'sentry/components/dropdownLink';
  9. import NewBooleanField from 'sentry/components/forms/booleanField';
  10. import Tooltip from 'sentry/components/tooltip';
  11. import {IconDownload, IconEllipsis} from 'sentry/icons';
  12. import {IconGrabbable} from 'sentry/icons/iconGrabbable';
  13. import {t, tn} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import {Project} from 'sentry/types';
  16. import {SamplingRule, SamplingRuleOperator} from 'sentry/types/sampling';
  17. import {formatPercentage} from 'sentry/utils/formatters';
  18. import {getInnerNameLabel, isUniformRule} from './utils';
  19. type Props = {
  20. dragging: boolean;
  21. /**
  22. * Hide the grab button if true.
  23. * This is used when the list has a single item, making sorting not possible.
  24. */
  25. hideGrabButton: boolean;
  26. listeners: DraggableSyntheticListeners;
  27. noPermission: boolean;
  28. onActivate: () => void;
  29. onDeleteRule: () => void;
  30. onEditRule: () => void;
  31. operator: SamplingRuleOperator;
  32. rule: SamplingRule;
  33. sorting: boolean;
  34. /**
  35. * If not empty, the activate rule toggle will be disabled.
  36. */
  37. upgradeSdkForProjects: Project['slug'][];
  38. grabAttributes?: UseDraggableArguments['attributes'];
  39. };
  40. export function Rule({
  41. dragging,
  42. rule,
  43. noPermission,
  44. onEditRule,
  45. onDeleteRule,
  46. onActivate,
  47. listeners,
  48. operator,
  49. grabAttributes,
  50. hideGrabButton,
  51. upgradeSdkForProjects,
  52. }: Props) {
  53. const isUniform = isUniformRule(rule);
  54. const canDelete = !noPermission && !isUniform;
  55. const canActivate = true; // TODO(sampling): Enabling this for demo purposes, change this back to `!upgradeSdkForProjects.length;` for LA
  56. const canDrag = !isUniform && !noPermission;
  57. return (
  58. <Fragment>
  59. <GrabColumn disabled={!canDrag}>
  60. {hideGrabButton ? null : (
  61. <Tooltip
  62. title={
  63. noPermission
  64. ? t('You do not have permission to reorder rules')
  65. : isUniform
  66. ? t('Uniform rules cannot be reordered')
  67. : undefined
  68. }
  69. containerDisplayMode="flex"
  70. >
  71. <IconGrabbableWrapper
  72. {...listeners}
  73. {...grabAttributes}
  74. aria-label={dragging ? t('Drop Rule') : t('Drag Rule')}
  75. aria-disabled={!canDrag}
  76. >
  77. <IconGrabbable />
  78. </IconGrabbableWrapper>
  79. </Tooltip>
  80. )}
  81. </GrabColumn>
  82. <OperatorColumn>
  83. <Operator>
  84. {operator === SamplingRuleOperator.IF
  85. ? t('If')
  86. : operator === SamplingRuleOperator.ELSE_IF
  87. ? t('Else if')
  88. : t('Else')}
  89. </Operator>
  90. </OperatorColumn>
  91. <ConditionColumn>
  92. {hideGrabButton && !rule.condition.inner.length
  93. ? t('All')
  94. : rule.condition.inner.map((condition, index) => (
  95. <Fragment key={index}>
  96. <ConditionName>{getInnerNameLabel(condition.name)}</ConditionName>
  97. <ConditionEqualOperator>{'='}</ConditionEqualOperator>
  98. {Array.isArray(condition.value) ? (
  99. <div>
  100. {[...condition.value].map((conditionValue, conditionValueIndex) => (
  101. <Fragment key={conditionValue}>
  102. <ConditionValue>{conditionValue}</ConditionValue>
  103. {conditionValueIndex !==
  104. (condition.value as string[]).length - 1 && (
  105. <ConditionSeparator>{'\u002C'}</ConditionSeparator>
  106. )}
  107. </Fragment>
  108. ))}
  109. </div>
  110. ) : (
  111. <ConditionValue>{String(condition.value)}</ConditionValue>
  112. )}
  113. </Fragment>
  114. ))}
  115. </ConditionColumn>
  116. <RateColumn>
  117. <SampleRate>{formatPercentage(rule.sampleRate)}</SampleRate>
  118. </RateColumn>
  119. <ActiveColumn>
  120. <GuideAnchor
  121. target="sampling_rule_toggle"
  122. onFinish={onActivate}
  123. disabled={!canActivate || !isUniform}
  124. >
  125. <Tooltip
  126. disabled={canActivate}
  127. title={
  128. !canActivate
  129. ? tn(
  130. 'To enable the rule, the recommended sdk version have to be updated',
  131. 'To enable the rule, the recommended sdk versions have to be updated',
  132. upgradeSdkForProjects.length
  133. )
  134. : undefined
  135. }
  136. >
  137. <ActiveToggle
  138. inline={false}
  139. hideControlState
  140. aria-label={rule.active ? t('Deactivate Rule') : t('Activate Rule')}
  141. onClick={onActivate}
  142. name="active"
  143. disabled={!canActivate}
  144. value={rule.active}
  145. />
  146. </Tooltip>
  147. </GuideAnchor>
  148. </ActiveColumn>
  149. <Column>
  150. <EllipisDropDownButton
  151. caret={false}
  152. customTitle={
  153. <Button aria-label={t('Actions')} icon={<IconEllipsis />} size="sm" />
  154. }
  155. anchorRight
  156. >
  157. <MenuItemActionLink
  158. shouldConfirm={false}
  159. icon={<IconDownload size="xs" />}
  160. title={t('Edit')}
  161. onClick={
  162. !noPermission
  163. ? onEditRule
  164. : event => {
  165. event?.stopPropagation();
  166. }
  167. }
  168. disabled={noPermission}
  169. >
  170. <Tooltip
  171. disabled={!noPermission}
  172. title={t('You do not have permission to edit sampling rules')}
  173. containerDisplayMode="block"
  174. >
  175. {t('Edit')}
  176. </Tooltip>
  177. </MenuItemActionLink>
  178. <MenuItemActionLink
  179. onAction={onDeleteRule}
  180. message={t('Are you sure you wish to delete this sampling rule?')}
  181. icon={<IconDownload size="xs" />}
  182. title={t('Delete')}
  183. disabled={!canDelete}
  184. priority="danger"
  185. shouldConfirm
  186. >
  187. <Tooltip
  188. disabled={canDelete}
  189. title={
  190. isUniform
  191. ? t("You can't delete the uniform rule")
  192. : t('You do not have permission to delete sampling rules')
  193. }
  194. containerDisplayMode="block"
  195. >
  196. {t('Delete')}
  197. </Tooltip>
  198. </MenuItemActionLink>
  199. </EllipisDropDownButton>
  200. </Column>
  201. </Fragment>
  202. );
  203. }
  204. export const Column = styled('div')`
  205. display: flex;
  206. padding: ${space(1)} ${space(2)};
  207. cursor: default;
  208. white-space: pre-wrap;
  209. word-break: break-all;
  210. `;
  211. export const GrabColumn = styled(Column)<{disabled?: boolean}>`
  212. [role='button'] {
  213. cursor: grab;
  214. }
  215. ${p =>
  216. p.disabled &&
  217. css`
  218. [role='button'] {
  219. cursor: not-allowed;
  220. }
  221. color: ${p.theme.disabled};
  222. `}
  223. display: none;
  224. @media (min-width: ${p => p.theme.breakpoints.small}) {
  225. display: flex;
  226. }
  227. `;
  228. export const OperatorColumn = styled(Column)`
  229. display: none;
  230. @media (min-width: ${p => p.theme.breakpoints.small}) {
  231. display: flex;
  232. }
  233. `;
  234. export const ConditionColumn = styled(Column)`
  235. display: flex;
  236. gap: ${space(1)};
  237. align-items: flex-start;
  238. flex-wrap: wrap;
  239. `;
  240. export const RateColumn = styled(Column)`
  241. justify-content: flex-end;
  242. text-align: right;
  243. `;
  244. export const ActiveColumn = styled(Column)`
  245. justify-content: center;
  246. text-align: center;
  247. display: none;
  248. @media (min-width: ${p => p.theme.breakpoints.small}) {
  249. display: flex;
  250. }
  251. `;
  252. const IconGrabbableWrapper = styled('div')`
  253. outline: none;
  254. display: flex;
  255. align-items: center;
  256. /* match the height of edit and delete buttons */
  257. height: 34px;
  258. `;
  259. const ConditionEqualOperator = styled('div')`
  260. color: ${p => p.theme.purple300};
  261. `;
  262. const Operator = styled('div')`
  263. color: ${p => p.theme.active};
  264. `;
  265. const SampleRate = styled('div')`
  266. white-space: pre-wrap;
  267. word-break: break-all;
  268. `;
  269. const ActiveToggle = styled(NewBooleanField)`
  270. padding: 0;
  271. height: 34px;
  272. justify-content: center;
  273. `;
  274. const ConditionName = styled('div')`
  275. color: ${p => p.theme.gray400};
  276. `;
  277. const ConditionValue = styled('span')`
  278. color: ${p => p.theme.gray300};
  279. `;
  280. const ConditionSeparator = styled(ConditionValue)`
  281. padding-right: ${space(0.5)};
  282. `;
  283. const EllipisDropDownButton = styled(DropdownLink)`
  284. display: flex;
  285. align-items: center;
  286. transition: none;
  287. `;