associations.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import {Link} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import ClippedBox from 'sentry/components/clippedBox';
  4. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  5. import {Hovercard} from 'sentry/components/hovercard';
  6. import Placeholder from 'sentry/components/placeholder';
  7. import TextOverflow from 'sentry/components/textOverflow';
  8. import {t, tct, tn} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {DebugIdBundleAssociation} from 'sentry/types/sourceMaps';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. function AssociationsBody({associations}: {associations: DebugIdBundleAssociation[]}) {
  13. const organization = useOrganization();
  14. return (
  15. <ClippedBoxWithoutPadding
  16. clipHeight={210}
  17. btnText={t('+ %s more', associations.length - associations.slice(0, 4).length)}
  18. buttonProps={{
  19. priority: 'default',
  20. borderless: true,
  21. }}
  22. >
  23. <NumericList>
  24. {associations.map(({release, dist}) => (
  25. <li key={release}>
  26. <ReleaseContent>
  27. <ReleaseLink
  28. to={`/organizations/${organization.slug}/releases/${release}/`}
  29. >
  30. <TextOverflow>{release}</TextOverflow>
  31. </ReleaseLink>
  32. <CopyToClipboardButton
  33. text={release}
  34. borderless
  35. size="zero"
  36. iconSize="sm"
  37. />
  38. </ReleaseContent>
  39. {!dist?.length ? (
  40. <NoAssociations>
  41. {t('No dists associated with this release')}
  42. </NoAssociations>
  43. ) : (
  44. tct('Dist: [dist]', {
  45. dist: typeof dist === 'string' ? dist : dist.join(', '),
  46. })
  47. )}
  48. </li>
  49. ))}
  50. </NumericList>
  51. </ClippedBoxWithoutPadding>
  52. );
  53. }
  54. type Props = {
  55. associations?: DebugIdBundleAssociation[];
  56. loading?: boolean;
  57. };
  58. export function Associations({associations = [], loading}: Props) {
  59. if (loading) {
  60. return <Placeholder width="200px" height="20px" />;
  61. }
  62. if (!associations.length) {
  63. return (
  64. <NoAssociations>{t('No releases associated with this bundle')}</NoAssociations>
  65. );
  66. }
  67. return (
  68. <div>
  69. <WiderHovercard
  70. position="right"
  71. body={<AssociationsBody associations={associations} />}
  72. header={t('Releases')}
  73. displayTimeout={0}
  74. showUnderline
  75. >
  76. {tn('%s Release', '%s Releases', associations.length)}
  77. </WiderHovercard>{' '}
  78. {t('associated')}
  79. </div>
  80. );
  81. }
  82. const NoAssociations = styled('div')`
  83. color: ${p => p.theme.disabled};
  84. `;
  85. const ReleaseContent = styled('div')`
  86. display: grid;
  87. grid-template-columns: 1fr max-content;
  88. gap: ${space(1)};
  89. align-items: center;
  90. `;
  91. const ReleaseLink = styled(Link)`
  92. overflow: hidden;
  93. `;
  94. // TODO(ui): Add a native numeric list to the List component
  95. const NumericList = styled('ol')`
  96. display: flex;
  97. flex-direction: column;
  98. gap: ${space(0.5)};
  99. margin: 0;
  100. `;
  101. const WiderHovercard = styled(Hovercard)`
  102. width: 320px;
  103. /* "Body" element */
  104. > div:last-child {
  105. transition: all 5s ease-in-out;
  106. overflow-x: hidden;
  107. overflow-y: scroll;
  108. max-height: 300px;
  109. }
  110. `;
  111. const ClippedBoxWithoutPadding = styled(ClippedBox)`
  112. padding: 0;
  113. /* "ClipFade" element */
  114. > div:last-child {
  115. background: ${p => p.theme.background};
  116. border-bottom: 0;
  117. padding: 0;
  118. }
  119. `;