codeSnippet.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import 'prism-sentry/index.css';
  2. import {Fragment, useEffect, useRef, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import Prism from 'prismjs';
  5. import Tooltip from 'sentry/components/tooltip';
  6. import {IconCopy} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import space from 'sentry/styles/space';
  9. interface CodeSnippetProps {
  10. children: string;
  11. language: string;
  12. filename?: string;
  13. hideActionBar?: boolean;
  14. }
  15. export function CodeSnippet({
  16. children,
  17. language,
  18. filename,
  19. hideActionBar,
  20. }: CodeSnippetProps) {
  21. const ref = useRef<HTMLModElement | null>(null);
  22. useEffect(
  23. () => void (ref.current && Prism.highlightElement(ref.current, false)),
  24. [children]
  25. );
  26. const [tooltipState, setTooltipState] = useState<'copy' | 'copied' | 'error'>('copy');
  27. const handleCopy = async () => {
  28. try {
  29. await navigator.clipboard.writeText(children);
  30. setTooltipState('copied');
  31. } catch (err) {
  32. setTooltipState('error');
  33. }
  34. };
  35. const tooltipTitle =
  36. tooltipState === 'copy'
  37. ? t('Copy')
  38. : tooltipState === 'copied'
  39. ? t('Copied')
  40. : t('Unable to copy');
  41. return (
  42. <Fragment>
  43. {!hideActionBar && (
  44. <CodeContainerActionBar>
  45. {filename && <span>{filename}</span>}
  46. <Tooltip delay={0} isHoverable={false} title={tooltipTitle} position="bottom">
  47. <UnstyledButton
  48. type="button"
  49. onClick={handleCopy}
  50. onMouseLeave={() => setTooltipState('copy')}
  51. >
  52. <IconCopy />
  53. </UnstyledButton>
  54. </Tooltip>
  55. </CodeContainerActionBar>
  56. )}
  57. <PreContainer unsetBorderRadiusTop={!hideActionBar}>
  58. <code ref={ref} className={`language-${language}`}>
  59. {children}
  60. </code>
  61. </PreContainer>
  62. </Fragment>
  63. );
  64. }
  65. const PreContainer = styled('pre')<{unsetBorderRadiusTop?: boolean}>`
  66. overflow-x: scroll;
  67. ${p =>
  68. p.unsetBorderRadiusTop
  69. ? `
  70. border-top-left-radius: 0px;
  71. border-top-right-radius: 0px;
  72. `
  73. : null}
  74. word-break: break-all;
  75. white-space: pre-wrap;
  76. code {
  77. white-space: pre;
  78. }
  79. `;
  80. const UnstyledButton = styled('button')`
  81. all: unset;
  82. cursor: pointer;
  83. `;
  84. // code blocks are globally styled by `prism-sentry`
  85. // its design tokens are slightly different than the app
  86. // so we've left it in charge of colors while overriding
  87. // css that breaks the experience
  88. const CodeContainerActionBar = styled(({children, ...props}) => (
  89. <div {...props}>
  90. <pre className="language-">{children}</pre>
  91. </div>
  92. ))`
  93. pre.language- {
  94. display: flex;
  95. justify-content: end;
  96. gap: ${space(1)};
  97. padding: ${space(1.5)};
  98. margin-bottom: 0px;
  99. border-bottom: 1px solid ${p => p.theme.purple200};
  100. border-radius: ${p => p.theme.borderRadiusTop};
  101. font-size: ${p => p.theme.fontSizeSmall};
  102. }
  103. `;