codeSnippet.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. // Prism components need to be imported after Prism
  2. // eslint-disable-next-line simple-import-sort/imports
  3. import Prism from 'prismjs';
  4. import 'prismjs/components/prism-bash.min';
  5. import {useEffect, useRef, useState} from 'react';
  6. import styled from '@emotion/styled';
  7. import copy from 'copy-text-to-clipboard';
  8. import {Button} from 'sentry/components/button';
  9. import {IconCopy} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. interface CodeSnippetProps {
  13. children: string;
  14. language: keyof typeof Prism.languages;
  15. className?: string;
  16. dark?: boolean;
  17. filename?: string;
  18. hideCopyButton?: boolean;
  19. onCopy?: (copiedCode: string) => void;
  20. }
  21. export function CodeSnippet({
  22. children,
  23. language,
  24. dark,
  25. filename,
  26. hideCopyButton,
  27. onCopy,
  28. className,
  29. }: CodeSnippetProps) {
  30. const ref = useRef<HTMLModElement | null>(null);
  31. useEffect(
  32. () => void (ref.current && Prism.highlightElement(ref.current, false)),
  33. [children]
  34. );
  35. const [tooltipState, setTooltipState] = useState<'copy' | 'copied' | 'error'>('copy');
  36. const handleCopy = () => {
  37. const copied = copy(children);
  38. onCopy?.(children);
  39. if (copied) {
  40. setTooltipState('copied');
  41. } else {
  42. setTooltipState('error');
  43. }
  44. };
  45. const tooltipTitle =
  46. tooltipState === 'copy'
  47. ? t('Copy')
  48. : tooltipState === 'copied'
  49. ? t('Copied')
  50. : t('Unable to copy');
  51. return (
  52. <Wrapper className={`${dark ? 'prism-dark ' : ''}${className ?? ''}`}>
  53. <Header hasFileName={!!filename}>
  54. {filename && <FileName>{filename}</FileName>}
  55. {!hideCopyButton && (
  56. <CopyButton
  57. type="button"
  58. size="xs"
  59. translucentBorder
  60. borderless={!!filename}
  61. onClick={handleCopy}
  62. title={tooltipTitle}
  63. tooltipProps={{delay: 0, isHoverable: false, position: 'left'}}
  64. onMouseLeave={() => setTooltipState('copy')}
  65. >
  66. <IconCopy size="xs" />
  67. </CopyButton>
  68. )}
  69. </Header>
  70. <pre className={`language-${String(language)}`}>
  71. <code ref={ref} className={`language-${String(language)}`}>
  72. {children}
  73. </code>
  74. </pre>
  75. </Wrapper>
  76. );
  77. }
  78. const Wrapper = styled('div')`
  79. position: relative;
  80. background: ${p => p.theme.backgroundSecondary};
  81. border-radius: ${p => p.theme.borderRadius};
  82. pre {
  83. margin: 0;
  84. }
  85. `;
  86. const Header = styled('div')<{hasFileName: boolean}>`
  87. display: flex;
  88. justify-content: space-between;
  89. align-items: center;
  90. font-family: ${p => p.theme.text.familyMono};
  91. font-size: ${p => p.theme.codeFontSize};
  92. color: ${p => p.theme.headingColor};
  93. font-weight: 600;
  94. z-index: 2;
  95. ${p =>
  96. p.hasFileName
  97. ? `
  98. padding: ${space(0.5)} 0;
  99. margin: 0 ${space(0.5)} 0 ${space(2)};
  100. border-bottom: solid 1px ${p.theme.innerBorder};
  101. `
  102. : `
  103. justify-content: flex-end;
  104. position: absolute;
  105. top: 0;
  106. right: 0;
  107. width: max-content;
  108. height: max-content;
  109. max-height: 100%;
  110. padding: ${space(1)};
  111. `}
  112. `;
  113. const FileName = styled('p')`
  114. ${p => p.theme.overflowEllipsis}
  115. margin: 0;
  116. `;
  117. const CopyButton = styled(Button)`
  118. color: ${p => p.theme.subText};
  119. transition: opacity 0.1s ease-out;
  120. opacity: 0;
  121. p + &, /* if preceded by FileName */
  122. div:hover > div > &, /* if Wrapper is hovered */
  123. &.focus-visible {
  124. opacity: 1;
  125. }
  126. `;