codeSnippet.tsx 3.4 KB

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