rule.tsx 9.1 KB

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