conditions.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import FieldRequiredBadge from 'sentry/components/forms/field/fieldRequiredBadge';
  5. import TextareaField from 'sentry/components/forms/textareaField';
  6. import {IconDelete} from 'sentry/icons/iconDelete';
  7. import {t} from 'sentry/locale';
  8. import space from 'sentry/styles/space';
  9. import {Project, Tag} from 'sentry/types';
  10. import {LegacyBrowser, SamplingInnerName} from 'sentry/types/sampling';
  11. import useApi from 'sentry/utils/useApi';
  12. import {
  13. addCustomTagPrefix,
  14. getInnerNameLabel,
  15. isCustomTagName,
  16. stripCustomTagPrefix,
  17. } from '../utils';
  18. import LegacyBrowsers from './legacyBrowsers';
  19. import {TagKeyAutocomplete} from './tagKeyAutocomplete';
  20. import {TagValueAutocomplete} from './tagValueAutocomplete';
  21. import {getMatchFieldPlaceholder, getTagKey} from './utils';
  22. type Condition = {
  23. category: SamplingInnerName | string; // string is used for custom tags
  24. legacyBrowsers?: Array<LegacyBrowser>;
  25. match?: string;
  26. };
  27. type Props = Pick<
  28. React.ComponentProps<typeof TagValueAutocomplete>,
  29. 'orgSlug' | 'projectId'
  30. > & {
  31. conditions: Condition[];
  32. onChange: <T extends keyof Condition>(
  33. index: number,
  34. field: T,
  35. value: Condition[T]
  36. ) => void;
  37. onDelete: (index: number) => void;
  38. projectSlug: Project['slug'];
  39. };
  40. function Conditions({
  41. conditions,
  42. orgSlug,
  43. projectId,
  44. projectSlug,
  45. onDelete,
  46. onChange,
  47. }: Props) {
  48. const api = useApi();
  49. const [tags, setTags] = useState<Tag[]>([]);
  50. useEffect(() => {
  51. async function fetchTags() {
  52. try {
  53. const response = await api.requestPromise(
  54. `/projects/${orgSlug}/${projectSlug}/tags/`,
  55. {query: {onlySamplingTags: 1}}
  56. );
  57. setTags(response);
  58. } catch {
  59. // Do nothing, just autocomplete won't suggest any results
  60. }
  61. }
  62. fetchTags();
  63. }, [api, orgSlug, projectSlug]);
  64. return (
  65. <Fragment>
  66. {conditions.map((condition, index) => {
  67. const {category, match, legacyBrowsers} = condition;
  68. const displayLegacyBrowsers = category === SamplingInnerName.EVENT_LEGACY_BROWSER;
  69. const isCustomTag = isCustomTagName(category);
  70. const isBooleanField =
  71. category === SamplingInnerName.EVENT_LOCALHOST ||
  72. category === SamplingInnerName.EVENT_WEB_CRAWLERS;
  73. displayLegacyBrowsers;
  74. const isAutoCompleteField =
  75. category === SamplingInnerName.EVENT_ENVIRONMENT ||
  76. category === SamplingInnerName.EVENT_RELEASE ||
  77. category === SamplingInnerName.EVENT_TRANSACTION ||
  78. category === SamplingInnerName.EVENT_OS_NAME ||
  79. category === SamplingInnerName.EVENT_DEVICE_FAMILY ||
  80. category === SamplingInnerName.EVENT_DEVICE_NAME ||
  81. category === SamplingInnerName.TRACE_ENVIRONMENT ||
  82. category === SamplingInnerName.TRACE_RELEASE ||
  83. category === SamplingInnerName.TRACE_TRANSACTION ||
  84. isCustomTag;
  85. return (
  86. <ConditionWrapper key={index}>
  87. <LeftCell>
  88. {isCustomTag ? (
  89. <TagKeyAutocomplete
  90. tags={tags}
  91. onChange={value =>
  92. onChange(index, 'category', addCustomTagPrefix(value))
  93. }
  94. value={stripCustomTagPrefix(category)}
  95. disabledOptions={conditions
  96. .filter(
  97. cond => isCustomTagName(cond.category) && cond.category !== category
  98. )
  99. .map(cond => stripCustomTagPrefix(cond.category))}
  100. />
  101. ) : (
  102. <span>
  103. {getInnerNameLabel(category)}
  104. <FieldRequiredBadge />
  105. </span>
  106. )}
  107. </LeftCell>
  108. <CenterCell>
  109. {!isBooleanField &&
  110. (isAutoCompleteField ? (
  111. <TagValueAutocomplete
  112. category={category}
  113. tagKey={getTagKey(condition)}
  114. orgSlug={orgSlug}
  115. projectId={projectId}
  116. value={match}
  117. onChange={value => onChange(index, 'match', value)}
  118. />
  119. ) : (
  120. <StyledTextareaField
  121. name="match"
  122. value={match}
  123. onChange={value => onChange(index, 'match', value)}
  124. placeholder={getMatchFieldPlaceholder(category)}
  125. inline={false}
  126. rows={1}
  127. autosize
  128. hideControlState
  129. flexibleControlStateSize
  130. required
  131. stacked
  132. />
  133. ))}
  134. </CenterCell>
  135. <RightCell>
  136. <Button
  137. onClick={() => onDelete(index)}
  138. icon={<IconDelete />}
  139. aria-label={t('Delete Condition')}
  140. />
  141. </RightCell>
  142. {displayLegacyBrowsers && (
  143. <LegacyBrowsers
  144. selectedLegacyBrowsers={legacyBrowsers}
  145. onChange={value => {
  146. onChange(index, 'legacyBrowsers', value);
  147. }}
  148. />
  149. )}
  150. </ConditionWrapper>
  151. );
  152. })}
  153. </Fragment>
  154. );
  155. }
  156. export default Conditions;
  157. const ConditionWrapper = styled('div')`
  158. display: grid;
  159. grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  160. align-items: flex-start;
  161. padding: ${space(1)} ${space(2)};
  162. :not(:last-child) {
  163. border-bottom: 1px solid ${p => p.theme.gray100};
  164. }
  165. @media (min-width: ${p => p.theme.breakpoints.small}) {
  166. grid-template-columns: minmax(0, 0.6fr) minmax(0, 1fr) max-content;
  167. }
  168. `;
  169. const Cell = styled('div')`
  170. min-height: 40px;
  171. display: inline-flex;
  172. align-items: center;
  173. `;
  174. const LeftCell = styled(Cell)`
  175. padding-right: ${space(1)};
  176. line-height: 16px;
  177. `;
  178. const CenterCell = styled(Cell)`
  179. padding-top: ${space(1)};
  180. grid-column: 1/-1;
  181. grid-row: 2/2;
  182. ${p => !p.children && 'display: none'};
  183. @media (min-width: ${p => p.theme.breakpoints.small}) {
  184. grid-column: auto;
  185. grid-row: auto;
  186. padding-top: 0;
  187. }
  188. `;
  189. const RightCell = styled(Cell)`
  190. justify-content: flex-end;
  191. padding-left: ${space(1)};
  192. `;
  193. const StyledTextareaField = styled(TextareaField)`
  194. padding-bottom: 0;
  195. width: 100%;
  196. `;