organizationSampleRateInput.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import type React from 'react';
  2. import {useEffect, useRef} from 'react';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {IconEdit} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
  10. import {useHasDynamicSamplingWriteAccess} from 'sentry/views/settings/dynamicSampling/utils/access';
  11. interface Props {
  12. help: React.ReactNode;
  13. label: React.ReactNode;
  14. onChange: (value: string) => void;
  15. previousValue: string;
  16. showPreviousValue: boolean;
  17. value: string;
  18. error?: string;
  19. isBulkEditActive?: boolean;
  20. isBulkEditEnabled?: boolean;
  21. onBulkEditChange?: (value: boolean) => void;
  22. }
  23. export function OrganizationSampleRateInput({
  24. value,
  25. onChange,
  26. isBulkEditEnabled,
  27. isBulkEditActive,
  28. label,
  29. help,
  30. error,
  31. previousValue,
  32. showPreviousValue,
  33. onBulkEditChange,
  34. }: Props) {
  35. const hasAccess = useHasDynamicSamplingWriteAccess();
  36. const inputRef = useRef<HTMLInputElement>(null);
  37. // Autofocus the input when bulk edit is activated
  38. useEffect(() => {
  39. if (isBulkEditActive) {
  40. inputRef.current?.focus();
  41. }
  42. }, [isBulkEditActive]);
  43. const showBulkEditButton = hasAccess && isBulkEditEnabled && !isBulkEditActive;
  44. return (
  45. <Wrapper>
  46. <Description>
  47. <Label>{label}</Label>
  48. <HelpText>{help}</HelpText>
  49. </Description>
  50. <InputWrapper>
  51. <FlexRow>
  52. {showBulkEditButton && (
  53. <Button
  54. title={t('Proportionally scale project rates')}
  55. aria-label={t('Proportionally scale project rates')}
  56. borderless
  57. size="sm"
  58. onClick={() => onBulkEditChange?.(true)}
  59. icon={<IconEdit />}
  60. />
  61. )}
  62. <Tooltip
  63. disabled={hasAccess}
  64. title={t('You do not have permission to change the sample rate.')}
  65. >
  66. <PercentInput
  67. type="number"
  68. disabled={!hasAccess || (isBulkEditEnabled && !isBulkEditActive)}
  69. value={value}
  70. size="sm"
  71. ref={inputRef}
  72. onKeyDown={event => {
  73. if (event.key === 'Enter' && isBulkEditActive) {
  74. event.preventDefault();
  75. inputRef.current?.blur();
  76. }
  77. }}
  78. onBlur={() => onBulkEditChange?.(false)}
  79. onChange={event => onChange(event.target.value)}
  80. />
  81. </Tooltip>
  82. </FlexRow>
  83. {error ? (
  84. <ErrorMessage>{error}</ErrorMessage>
  85. ) : showPreviousValue ? (
  86. <PreviousValue>{t('previous: %f%%', previousValue)}</PreviousValue>
  87. ) : value === '100' ? (
  88. <AllDataStoredMessage>{t('All spans are stored')}</AllDataStoredMessage>
  89. ) : null}
  90. </InputWrapper>
  91. </Wrapper>
  92. );
  93. }
  94. const FlexRow = styled('div')`
  95. display: flex;
  96. gap: ${space(1)};
  97. `;
  98. const Wrapper = styled(FlexRow)`
  99. padding: ${space(1.5)} ${space(2)} ${space(1)};
  100. border-bottom: 1px solid ${p => p.theme.innerBorder};
  101. gap: ${space(4)};
  102. `;
  103. const Description = styled('div')`
  104. flex: 1;
  105. display: flex;
  106. flex-direction: column;
  107. gap: ${space(0.5)};
  108. padding-bottom: ${space(0.5)};
  109. `;
  110. const Label = styled('label')`
  111. margin-bottom: 0;
  112. `;
  113. const HelpText = styled('div')`
  114. font-size: ${p => p.theme.fontSizeSmall};
  115. color: ${p => p.theme.subText};
  116. `;
  117. const PreviousValue = styled('span')`
  118. font-size: ${p => p.theme.fontSizeExtraSmall};
  119. color: ${p => p.theme.subText};
  120. `;
  121. const ErrorMessage = styled('span')`
  122. font-size: ${p => p.theme.fontSizeExtraSmall};
  123. color: ${p => p.theme.error};
  124. `;
  125. const AllDataStoredMessage = styled('span')`
  126. font-size: ${p => p.theme.fontSizeExtraSmall};
  127. color: ${p => p.theme.success};
  128. `;
  129. const InputWrapper = styled('div')`
  130. height: 50px;
  131. display: flex;
  132. flex-direction: column;
  133. gap: 4px;
  134. flex-shrink: 0;
  135. align-items: flex-end;
  136. `;