rule.tsx 8.7 KB

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