projectGeneralSettings.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import {createFilter} from 'react-select';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import {Field} from 'sentry/components/forms/types';
  5. import platforms from 'sentry/data/platforms';
  6. import {t, tct, tn} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {convertMultilineFieldValue, extractMultilineFields} from 'sentry/utils';
  9. import getDynamicText from 'sentry/utils/getDynamicText';
  10. import slugify from 'sentry/utils/slugify';
  11. // Export route to make these forms searchable by label/help
  12. export const route = '/settings/:orgId/projects/:projectId/';
  13. const getResolveAgeAllowedValues = () => {
  14. let i = 0;
  15. const values: number[] = [];
  16. while (i <= 720) {
  17. values.push(i);
  18. if (i < 12) {
  19. i += 1;
  20. } else if (i < 24) {
  21. i += 3;
  22. } else if (i < 36) {
  23. i += 6;
  24. } else if (i < 48) {
  25. i += 12;
  26. } else {
  27. i += 24;
  28. }
  29. }
  30. return values;
  31. };
  32. const RESOLVE_AGE_ALLOWED_VALUES = getResolveAgeAllowedValues();
  33. const ORG_DISABLED_REASON = t(
  34. "This option is enforced by your organization's settings and cannot be customized per-project."
  35. );
  36. const PlatformWrapper = styled('div')`
  37. display: flex;
  38. align-items: center;
  39. `;
  40. const StyledPlatformIcon = styled(PlatformIcon)`
  41. margin-right: ${space(1)};
  42. `;
  43. export const fields: Record<string, Field> = {
  44. name: {
  45. name: 'name',
  46. type: 'string',
  47. required: true,
  48. label: t('Name'),
  49. placeholder: t('my-awesome-project'),
  50. help: t('A name for this project'),
  51. transformInput: slugify,
  52. getData: (data: {name?: string}) => {
  53. return {
  54. name: data.name,
  55. slug: data.name,
  56. };
  57. },
  58. saveOnBlur: false,
  59. saveMessageAlertType: 'info',
  60. saveMessage: t('You will be redirected to the new project slug after saving'),
  61. },
  62. platform: {
  63. name: 'platform',
  64. type: 'select',
  65. label: t('Platform'),
  66. options: platforms.map(({id, name}) => ({
  67. value: id,
  68. label: (
  69. <PlatformWrapper key={id}>
  70. <StyledPlatformIcon platform={id} />
  71. {name}
  72. </PlatformWrapper>
  73. ),
  74. })),
  75. help: t('The primary platform for this project'),
  76. filterOption: createFilter({
  77. stringify: option => {
  78. const matchedPlatform = platforms.find(({id}) => id === option.value);
  79. return `${matchedPlatform?.name} ${option.value}`;
  80. },
  81. }),
  82. },
  83. // TODO(recap): Move this to a separate page or debug files one, not general settings
  84. recapServerUrl: {
  85. name: 'recapServerUrl',
  86. type: 'string',
  87. placeholder: t('URL'),
  88. label: t('Recap Server URL'),
  89. help: t('URL to the Recap Server events should be polled from'),
  90. },
  91. recapServerToken: {
  92. name: 'recapServerToken',
  93. type: 'string',
  94. placeholder: t('Token'),
  95. label: t('Recap Server Token'),
  96. help: t('Auth Token to the configured Recap Server'),
  97. },
  98. subjectPrefix: {
  99. name: 'subjectPrefix',
  100. type: 'string',
  101. label: t('Subject Prefix'),
  102. placeholder: t('e.g. [my-org]'),
  103. help: t('Choose a custom prefix for emails from this project'),
  104. },
  105. resolveAge: {
  106. name: 'resolveAge',
  107. type: 'range',
  108. allowedValues: RESOLVE_AGE_ALLOWED_VALUES,
  109. label: t('Auto Resolve'),
  110. help: t(
  111. "Automatically resolve an issue if it hasn't been seen for this amount of time"
  112. ),
  113. formatLabel: val => {
  114. val = Number(val);
  115. if (val === 0) {
  116. return t('Disabled');
  117. }
  118. if (val > 23 && val % 24 === 0) {
  119. // Based on allowed values, val % 24 should always be true
  120. val = val / 24;
  121. return tn('%s day', '%s days', val);
  122. }
  123. return tn('%s hour', '%s hours', val);
  124. },
  125. saveOnBlur: false,
  126. saveMessage: tct(
  127. '[strong:Caution]: Enabling auto resolve will immediately resolve anything that has not been seen within this period of time. There is no undo!',
  128. {
  129. strong: <strong />,
  130. }
  131. ),
  132. saveMessageAlertType: 'warning',
  133. },
  134. allowedDomains: {
  135. name: 'allowedDomains',
  136. type: 'string',
  137. multiline: true,
  138. autosize: true,
  139. maxRows: 10,
  140. rows: 1,
  141. placeholder: t('https://example.com or example.com'),
  142. label: t('Allowed Domains'),
  143. help: t(
  144. 'Examples: https://example.com, *, *.example.com, *:80. Separate multiple entries with a newline'
  145. ),
  146. getValue: val => extractMultilineFields(val),
  147. setValue: val => convertMultilineFieldValue(val),
  148. },
  149. scrapeJavaScript: {
  150. name: 'scrapeJavaScript',
  151. type: 'boolean',
  152. // if this is off for the organization, it cannot be enabled for the project
  153. disabled: ({organization, name}) => !organization[name],
  154. disabledReason: ORG_DISABLED_REASON,
  155. // `props` are the props given to FormField
  156. setValue: (val, props) => props.organization && props.organization[props.name] && val,
  157. label: t('Enable JavaScript source fetching'),
  158. help: t('Allow Sentry to scrape missing JavaScript source context when possible'),
  159. },
  160. securityToken: {
  161. name: 'securityToken',
  162. type: 'string',
  163. label: t('Security Token'),
  164. help: t(
  165. 'Outbound requests matching Allowed Domains will have the header "{token_header}: {token}" appended'
  166. ),
  167. setValue: value => getDynamicText({value, fixed: '__SECURITY_TOKEN__'}),
  168. },
  169. securityTokenHeader: {
  170. name: 'securityTokenHeader',
  171. type: 'string',
  172. placeholder: t('X-Sentry-Token'),
  173. label: t('Security Token Header'),
  174. help: t(
  175. 'Outbound requests matching Allowed Domains will have the header "{token_header}: {token}" appended'
  176. ),
  177. },
  178. verifySSL: {
  179. name: 'verifySSL',
  180. type: 'boolean',
  181. label: t('Verify TLS/SSL'),
  182. help: t('Outbound requests will verify TLS (sometimes known as SSL) connections'),
  183. },
  184. };