index.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import {isValidElement} from 'react';
  2. import styled from '@emotion/styled';
  3. import isArray from 'lodash/isArray';
  4. import isNumber from 'lodash/isNumber';
  5. import isString from 'lodash/isString';
  6. import AnnotatedText from 'sentry/components/events/meta/annotatedText';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import {IconOpen} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {Meta} from 'sentry/types';
  11. import {isUrl} from 'sentry/utils';
  12. import Toggle from './toggle';
  13. import {analyzeStringForRepr, naturalCaseInsensitiveSort} from './utils';
  14. type Value = null | string | boolean | number | {[key: string]: Value} | Value[];
  15. type Props = React.HTMLAttributes<HTMLPreElement> & {
  16. data?: Value;
  17. jsonConsts?: boolean;
  18. maxDefaultDepth?: number;
  19. meta?: Meta;
  20. preserveQuotes?: boolean;
  21. withAnnotatedText?: boolean;
  22. };
  23. function walk({
  24. depth,
  25. value,
  26. maxDefaultDepth: maxDepth = 2,
  27. preserveQuotes,
  28. withAnnotatedText,
  29. jsonConsts,
  30. meta,
  31. }: {
  32. depth: number;
  33. value: Value;
  34. } & Pick<
  35. Props,
  36. 'withAnnotatedText' | 'preserveQuotes' | 'jsonConsts' | 'meta' | 'maxDefaultDepth'
  37. >) {
  38. let i = 0;
  39. const children: React.ReactNode[] = [];
  40. if (value === null) {
  41. return (
  42. <span className="val-null">
  43. <AnnotatedText value={jsonConsts ? 'null' : 'None'} meta={meta} />
  44. </span>
  45. );
  46. }
  47. if (value === true || value === false) {
  48. return (
  49. <span className="val-bool">
  50. <AnnotatedText
  51. value={jsonConsts ? (value ? 'true' : 'false') : value ? 'True' : 'False'}
  52. meta={meta}
  53. />
  54. </span>
  55. );
  56. }
  57. if (isString(value)) {
  58. const valueInfo = analyzeStringForRepr(value);
  59. const valueToBeReturned = withAnnotatedText ? (
  60. <AnnotatedText value={valueInfo.repr} meta={meta} />
  61. ) : (
  62. valueInfo.repr
  63. );
  64. const out = [
  65. <span
  66. key="value"
  67. className={
  68. (valueInfo.isString ? 'val-string' : '') +
  69. (valueInfo.isStripped ? ' val-stripped' : '') +
  70. (valueInfo.isMultiLine ? ' val-string-multiline' : '')
  71. }
  72. >
  73. {preserveQuotes ? `"${valueToBeReturned}"` : valueToBeReturned}
  74. </span>,
  75. ];
  76. if (valueInfo.isString && isUrl(value)) {
  77. out.push(
  78. <ExternalLink key="external" href={value} className="external-icon">
  79. <StyledIconOpen size="xs" aria-label={t('Open link')} />
  80. </ExternalLink>
  81. );
  82. }
  83. return out;
  84. }
  85. if (isNumber(value)) {
  86. const valueToBeReturned =
  87. withAnnotatedText && meta ? <AnnotatedText value={value} meta={meta} /> : value;
  88. return <span>{valueToBeReturned}</span>;
  89. }
  90. if (isArray(value)) {
  91. for (i = 0; i < value.length; i++) {
  92. children.push(
  93. <span className="val-array-item" key={i}>
  94. {walk({
  95. value: value[i],
  96. depth: depth + 1,
  97. preserveQuotes,
  98. withAnnotatedText,
  99. jsonConsts,
  100. meta,
  101. })}
  102. {i < value.length - 1 ? <span className="val-array-sep">{', '}</span> : null}
  103. </span>
  104. );
  105. }
  106. return (
  107. <span className="val-array">
  108. <span className="val-array-marker">{'['}</span>
  109. <Toggle highUp={depth <= maxDepth} wrapClassName="val-array-items">
  110. {children}
  111. </Toggle>
  112. <span className="val-array-marker">{']'}</span>
  113. </span>
  114. );
  115. }
  116. if (isValidElement(value)) {
  117. return value;
  118. }
  119. const keys = Object.keys(value);
  120. keys.sort(naturalCaseInsensitiveSort);
  121. for (i = 0; i < keys.length; i++) {
  122. const key = keys[i];
  123. children.push(
  124. <span className="val-dict-pair" key={key}>
  125. <span className="val-dict-key">
  126. <span className="val-string">{preserveQuotes ? `"${key}"` : key}</span>
  127. </span>
  128. <span className="val-dict-col">{': '}</span>
  129. <span className="val-dict-value">
  130. {walk({
  131. value: value[key],
  132. depth: depth + 1,
  133. preserveQuotes,
  134. withAnnotatedText,
  135. jsonConsts,
  136. meta,
  137. })}
  138. {i < keys.length - 1 ? <span className="val-dict-sep">{', '}</span> : null}
  139. </span>
  140. </span>
  141. );
  142. }
  143. return (
  144. <span className="val-dict">
  145. <span className="val-dict-marker">{'{'}</span>
  146. <Toggle highUp={depth <= maxDepth - 1} wrapClassName="val-dict-items">
  147. {children}
  148. </Toggle>
  149. <span className="val-dict-marker">{'}'}</span>
  150. </span>
  151. );
  152. }
  153. function ContextData({
  154. children,
  155. meta,
  156. jsonConsts,
  157. maxDefaultDepth,
  158. data = null,
  159. preserveQuotes = false,
  160. withAnnotatedText = false,
  161. ...props
  162. }: Props) {
  163. return (
  164. <pre {...props}>
  165. {walk({
  166. value: data,
  167. depth: 0,
  168. maxDefaultDepth,
  169. meta,
  170. jsonConsts,
  171. withAnnotatedText,
  172. preserveQuotes,
  173. })}
  174. {children}
  175. </pre>
  176. );
  177. }
  178. export default ContextData;
  179. const StyledIconOpen = styled(IconOpen)`
  180. position: relative;
  181. top: 1px;
  182. `;