headerItem.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import {forwardRef} from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import omit from 'lodash/omit';
  5. import Link from 'sentry/components/links/link';
  6. import Tooltip from 'sentry/components/tooltip';
  7. import {IconChevron, IconClose, IconInfo, IconLock, IconSettings} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import {Theme} from 'sentry/utils/theme';
  11. type DefaultProps = {
  12. allowClear: boolean;
  13. };
  14. type Props = {
  15. icon: React.ReactNode;
  16. forwardedRef?: React.Ref<HTMLDivElement>;
  17. hasChanges?: boolean;
  18. hasSelected?: boolean;
  19. hint?: string;
  20. isOpen?: boolean;
  21. loading?: boolean;
  22. locked?: boolean;
  23. lockedMessage?: React.ReactNode;
  24. onClear?: () => void;
  25. settingsLink?: string;
  26. } & Partial<DefaultProps> &
  27. React.HTMLAttributes<HTMLDivElement>;
  28. function HeaderItem({
  29. children,
  30. isOpen,
  31. hasSelected,
  32. icon,
  33. locked,
  34. lockedMessage,
  35. settingsLink,
  36. hint,
  37. loading,
  38. forwardedRef,
  39. onClear,
  40. allowClear = true,
  41. ...props
  42. }: Props) {
  43. const handleClear = (e: React.MouseEvent) => {
  44. e.stopPropagation();
  45. onClear?.();
  46. };
  47. const textColorProps = {
  48. locked,
  49. isOpen,
  50. hasSelected,
  51. };
  52. return (
  53. <StyledHeaderItem
  54. ref={forwardedRef}
  55. loading={!!loading}
  56. {...omit(props, 'onClear')}
  57. {...textColorProps}
  58. >
  59. <IconContainer {...textColorProps}>{icon}</IconContainer>
  60. <Content>
  61. <StyledContent>{children}</StyledContent>
  62. {settingsLink && (
  63. <SettingsIconLink to={settingsLink}>
  64. <IconSettings />
  65. </SettingsIconLink>
  66. )}
  67. </Content>
  68. {hint && (
  69. <Hint>
  70. <Tooltip title={hint} position="bottom">
  71. <IconInfo size="sm" />
  72. </Tooltip>
  73. </Hint>
  74. )}
  75. {hasSelected && !locked && allowClear && (
  76. <StyledClose {...textColorProps} onClick={handleClear} />
  77. )}
  78. {!locked && !loading && (
  79. <ChevronWrapper>
  80. <StyledChevron isOpen={!!isOpen} direction={isOpen ? 'up' : 'down'} size="sm" />
  81. </ChevronWrapper>
  82. )}
  83. {locked && (
  84. <Tooltip title={lockedMessage || t('This selection is locked')} position="bottom">
  85. <StyledLock color="gray300" isSolid />
  86. </Tooltip>
  87. )}
  88. </StyledHeaderItem>
  89. );
  90. }
  91. // Infer props here because of styled/theme
  92. const getColor = (p: ColorProps & {theme: Theme}) => {
  93. if (p.locked) {
  94. return p.theme.gray300;
  95. }
  96. return p.isOpen || p.hasSelected ? p.theme.textColor : p.theme.gray300;
  97. };
  98. type ColorProps = {
  99. hasSelected?: boolean;
  100. isOpen?: boolean;
  101. locked?: boolean;
  102. };
  103. const StyledHeaderItem = styled('div', {
  104. shouldForwardProp: p => typeof p === 'string' && isPropValid(p) && p !== 'loading',
  105. })<
  106. ColorProps & {
  107. loading: boolean;
  108. }
  109. >`
  110. display: flex;
  111. padding: 0 ${space(4)};
  112. align-items: center;
  113. cursor: ${p => (p.loading ? 'progress' : p.locked ? 'text' : 'pointer')};
  114. color: ${getColor};
  115. transition: 0.1s color;
  116. user-select: none;
  117. `;
  118. const Content = styled('div')`
  119. display: flex;
  120. flex: 1;
  121. width: 0;
  122. white-space: nowrap;
  123. overflow: hidden;
  124. margin-right: ${space(1.5)};
  125. `;
  126. const StyledContent = styled('div')`
  127. overflow: hidden;
  128. text-overflow: ellipsis;
  129. `;
  130. const IconContainer = styled('span', {shouldForwardProp: isPropValid})<ColorProps>`
  131. color: ${getColor};
  132. margin-right: ${space(1.5)};
  133. display: flex;
  134. font-size: ${p => p.theme.fontSizeMedium};
  135. `;
  136. const Hint = styled('div')`
  137. position: relative;
  138. top: ${space(0.25)};
  139. margin-right: ${space(1)};
  140. `;
  141. const StyledClose = styled(IconClose, {shouldForwardProp: isPropValid})<ColorProps>`
  142. color: ${getColor};
  143. height: ${space(1.5)};
  144. width: ${space(1.5)};
  145. stroke-width: 1.5;
  146. padding: ${space(1)};
  147. box-sizing: content-box;
  148. margin: -${space(1)} 0px -${space(1)} -${space(1)};
  149. `;
  150. const ChevronWrapper = styled('div')`
  151. width: ${space(2)};
  152. height: ${space(2)};
  153. display: flex;
  154. align-items: center;
  155. justify-content: center;
  156. `;
  157. const StyledChevron = styled(IconChevron, {shouldForwardProp: isPropValid})<{
  158. isOpen: boolean;
  159. }>`
  160. color: ${getColor};
  161. `;
  162. const SettingsIconLink = styled(Link)`
  163. color: ${p => p.theme.gray300};
  164. align-items: center;
  165. display: inline-flex;
  166. justify-content: space-between;
  167. margin-right: ${space(1.5)};
  168. margin-left: ${space(1.0)};
  169. transition: 0.5s opacity ease-out;
  170. &:hover {
  171. color: ${p => p.theme.textColor};
  172. }
  173. `;
  174. const StyledLock = styled(IconLock)`
  175. margin-top: ${space(0.75)};
  176. stroke-width: 1.5;
  177. `;
  178. export default forwardRef<HTMLDivElement, Props>((props, ref) => (
  179. <HeaderItem forwardedRef={ref} {...props} />
  180. ));