integrationRow.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import styled from '@emotion/styled';
  2. import startCase from 'lodash/startCase';
  3. import Alert from 'sentry/components/alert';
  4. import Button from 'sentry/components/button';
  5. import Link from 'sentry/components/links/link';
  6. import {PanelItem} from 'sentry/components/panels';
  7. import {t} from 'sentry/locale';
  8. import PluginIcon from 'sentry/plugins/components/pluginIcon';
  9. import space from 'sentry/styles/space';
  10. import {
  11. IntegrationInstallationStatus,
  12. Organization,
  13. PluginWithProjectList,
  14. SentryApp,
  15. } from 'sentry/types';
  16. import {
  17. convertIntegrationTypeToSnakeCase,
  18. trackIntegrationAnalytics,
  19. } from 'sentry/utils/integrationUtil';
  20. import AlertContainer from './integrationAlertContainer';
  21. import IntegrationStatus from './integrationStatus';
  22. import PluginDeprecationAlert from './pluginDeprecationAlert';
  23. type Props = {
  24. categories: string[];
  25. configurations: number;
  26. displayName: string;
  27. organization: Organization;
  28. publishStatus: 'unpublished' | 'published' | 'internal';
  29. slug: string;
  30. type: 'plugin' | 'firstParty' | 'sentryApp' | 'docIntegration';
  31. /**
  32. * If provided, render an alert message with this text.
  33. */
  34. alertText?: string;
  35. customAlert?: React.ReactNode;
  36. customIcon?: React.ReactNode;
  37. plugin?: PluginWithProjectList;
  38. /**
  39. * If `alertText` was provided, this text overrides the "Resolve now" message
  40. * in the alert.
  41. */
  42. resolveText?: string;
  43. status?: IntegrationInstallationStatus;
  44. };
  45. const urlMap = {
  46. plugin: 'plugins',
  47. firstParty: 'integrations',
  48. sentryApp: 'sentry-apps',
  49. docIntegration: 'document-integrations',
  50. };
  51. const IntegrationRow = (props: Props) => {
  52. const {
  53. organization,
  54. type,
  55. slug,
  56. displayName,
  57. status,
  58. publishStatus,
  59. configurations,
  60. categories,
  61. alertText,
  62. resolveText,
  63. plugin,
  64. customAlert,
  65. customIcon,
  66. } = props;
  67. const baseUrl =
  68. publishStatus === 'internal'
  69. ? `/settings/${organization.slug}/developer-settings/${slug}/`
  70. : `/settings/${organization.slug}/${urlMap[type]}/${slug}/`;
  71. const renderDetails = () => {
  72. if (type === 'sentryApp') {
  73. return publishStatus !== 'published' && <PublishStatus status={publishStatus} />;
  74. }
  75. // TODO: Use proper translations
  76. return configurations > 0 ? (
  77. <StyledLink to={`${baseUrl}?tab=configurations`}>{`${configurations} Configuration${
  78. configurations > 1 ? 's' : ''
  79. }`}</StyledLink>
  80. ) : null;
  81. };
  82. const renderStatus = () => {
  83. // status should be undefined for document integrations
  84. if (status) {
  85. return <IntegrationStatus status={status} />;
  86. }
  87. return <LearnMore to={baseUrl}>{t('Learn More')}</LearnMore>;
  88. };
  89. return (
  90. <PanelRow noPadding data-test-id={slug}>
  91. <FlexContainer>
  92. {customIcon ?? <PluginIcon size={36} pluginId={slug} />}
  93. <TitleContainer>
  94. <IntegrationName to={baseUrl}>{displayName}</IntegrationName>
  95. <IntegrationDetails>
  96. {renderStatus()}
  97. {renderDetails()}
  98. </IntegrationDetails>
  99. </TitleContainer>
  100. <TagsContainer>
  101. {categories?.map(category => (
  102. <CategoryTag
  103. key={category}
  104. category={category === 'api' ? 'API' : startCase(category)}
  105. priority={category === publishStatus}
  106. />
  107. ))}
  108. </TagsContainer>
  109. </FlexContainer>
  110. {alertText && (
  111. <AlertContainer>
  112. <Alert
  113. type="warning"
  114. showIcon
  115. trailingItems={
  116. <ResolveNowButton
  117. href={`${baseUrl}?tab=configurations&referrer=directory_resolve_now`}
  118. size="xs"
  119. onClick={() =>
  120. trackIntegrationAnalytics('integrations.resolve_now_clicked', {
  121. integration_type: convertIntegrationTypeToSnakeCase(type),
  122. integration: slug,
  123. organization,
  124. })
  125. }
  126. >
  127. {resolveText || t('Resolve Now')}
  128. </ResolveNowButton>
  129. }
  130. >
  131. {alertText}
  132. </Alert>
  133. </AlertContainer>
  134. )}
  135. {customAlert}
  136. {plugin?.deprecationDate && (
  137. <PluginDeprecationAlertWrapper>
  138. <PluginDeprecationAlert organization={organization} plugin={plugin} />
  139. </PluginDeprecationAlertWrapper>
  140. )}
  141. </PanelRow>
  142. );
  143. };
  144. const PluginDeprecationAlertWrapper = styled('div')`
  145. padding: 0px ${space(3)} 0px 68px;
  146. `;
  147. const PanelRow = styled(PanelItem)`
  148. flex-direction: column;
  149. `;
  150. const FlexContainer = styled('div')`
  151. display: flex;
  152. align-items: center;
  153. padding: ${space(2)};
  154. `;
  155. const TitleContainer = styled('div')`
  156. flex: 1;
  157. padding: 0 16px;
  158. white-space: nowrap;
  159. `;
  160. const TagsContainer = styled('div')`
  161. flex: 3;
  162. text-align: right;
  163. padding: 0 ${space(2)};
  164. `;
  165. const IntegrationName = styled(Link)`
  166. font-weight: bold;
  167. `;
  168. const IntegrationDetails = styled('div')`
  169. display: flex;
  170. align-items: center;
  171. font-size: ${p => p.theme.fontSizeSmall};
  172. `;
  173. const StyledLink = styled(Link)`
  174. color: ${p => p.theme.gray300};
  175. &:before {
  176. content: '|';
  177. color: ${p => p.theme.gray200};
  178. margin-right: ${space(0.75)};
  179. }
  180. `;
  181. const LearnMore = styled(Link)`
  182. color: ${p => p.theme.gray300};
  183. `;
  184. type PublishStatusProps = {status: SentryApp['status']; theme?: any};
  185. const PublishStatus = styled(({status, ...props}: PublishStatusProps) => (
  186. <div {...props}>{t(`${status}`)}</div>
  187. ))`
  188. color: ${(props: PublishStatusProps) =>
  189. props.status === 'published' ? props.theme.success : props.theme.gray300};
  190. font-weight: light;
  191. margin-right: ${space(0.75)};
  192. text-transform: capitalize;
  193. &:before {
  194. content: '|';
  195. color: ${p => p.theme.gray200};
  196. margin-right: ${space(0.75)};
  197. font-weight: normal;
  198. }
  199. `;
  200. // TODO(Priscila): Replace this component with the Tag component
  201. const CategoryTag = styled(
  202. ({
  203. priority: _priority,
  204. category,
  205. ...p
  206. }: {
  207. category: string;
  208. priority: boolean;
  209. theme?: any;
  210. }) => <div {...p}>{category}</div>
  211. )`
  212. display: inline-block;
  213. padding: 1px 10px;
  214. background: ${p => (p.priority ? p.theme.purple200 : p.theme.gray100)};
  215. border-radius: 20px;
  216. font-size: ${space(1.5)};
  217. margin: ${space(0.25)} ${space(0.5)};
  218. line-height: ${space(3)};
  219. text-align: center;
  220. color: ${p => (p.priority ? p.theme.white : p.theme.gray500)};
  221. `;
  222. const ResolveNowButton = styled(Button)`
  223. color: ${p => p.theme.subText};
  224. float: right;
  225. `;
  226. export default IntegrationRow;