contextData.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import React 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 'app/components/events/meta/annotatedText';
  7. import ExternalLink from 'app/components/links/externalLink';
  8. import {IconAdd, IconOpen, IconSubtract} from 'app/icons';
  9. import {Meta} from 'app/types';
  10. import {isUrl} from 'app/utils';
  11. function looksLikeObjectRepr(value: string) {
  12. const a = value[0];
  13. const z = value[value.length - 1];
  14. if (a === '<' && z === '>') {
  15. return true;
  16. } else if (a === '[' && z === ']') {
  17. return true;
  18. } else if (a === '(' && z === ')') {
  19. return true;
  20. } else if (z === ')' && value.match(/^[\w\d._-]+\(/)) {
  21. return true;
  22. }
  23. return false;
  24. }
  25. function looksLikeMultiLineString(value: string) {
  26. return !!value.match(/[\r\n]/);
  27. }
  28. function padNumbersInString(string: string) {
  29. return string.replace(/(\d+)/g, (num: string) => {
  30. let isNegative = false;
  31. let realNum = parseInt(num, 10);
  32. if (realNum < 0) {
  33. realNum *= -1;
  34. isNegative = true;
  35. }
  36. let s = '0000000000000' + realNum;
  37. s = s.substr(s.length - (isNegative ? 11 : 12));
  38. if (isNegative) {
  39. s = '-' + s;
  40. }
  41. return s;
  42. });
  43. }
  44. function naturalCaseInsensitiveSort(a: string, b: string) {
  45. a = padNumbersInString(a).toLowerCase();
  46. b = padNumbersInString(b).toLowerCase();
  47. return a === b ? 0 : a < b ? -1 : 1;
  48. }
  49. function analyzeStringForRepr(value: string) {
  50. const rv = {
  51. repr: value,
  52. isString: true,
  53. isMultiLine: false,
  54. isStripped: false,
  55. };
  56. // stripped for security reasons
  57. if (value.match(/^['"]?\*{8,}['"]?$/)) {
  58. rv.isStripped = true;
  59. return rv;
  60. }
  61. if (looksLikeObjectRepr(value)) {
  62. rv.isString = false;
  63. } else {
  64. rv.isMultiLine = looksLikeMultiLineString(value);
  65. }
  66. return rv;
  67. }
  68. type ToggleWrapProps = {
  69. highUp: boolean;
  70. wrapClassName: string;
  71. };
  72. type ToggleWrapState = {
  73. toggled: boolean;
  74. };
  75. class ToggleWrap extends React.Component<ToggleWrapProps, ToggleWrapState> {
  76. state: ToggleWrapState = {toggled: false};
  77. render() {
  78. if (React.Children.count(this.props.children) === 0) {
  79. return null;
  80. }
  81. const {wrapClassName, children} = this.props;
  82. const wrappedChildren = <span className={wrapClassName}>{children}</span>;
  83. if (this.props.highUp) {
  84. return wrappedChildren;
  85. }
  86. return (
  87. <span>
  88. <ToggleIcon
  89. isOpen={this.state.toggled}
  90. href="#"
  91. onClick={evt => {
  92. this.setState(state => ({toggled: !state.toggled}));
  93. evt.preventDefault();
  94. }}
  95. >
  96. {this.state.toggled ? (
  97. <IconSubtract size="9px" color="white" />
  98. ) : (
  99. <IconAdd size="9px" color="white" />
  100. )}
  101. </ToggleIcon>
  102. {this.state.toggled && wrappedChildren}
  103. </span>
  104. );
  105. }
  106. }
  107. type Value = null | string | boolean | number | {[key: string]: Value} | Value[];
  108. type Props = React.HTMLAttributes<HTMLPreElement> & {
  109. data: Value;
  110. preserveQuotes?: boolean;
  111. withAnnotatedText?: boolean;
  112. maxDefaultDepth?: number;
  113. meta?: Meta;
  114. jsonConsts?: boolean;
  115. };
  116. type State = {
  117. data: Value;
  118. withAnnotatedText: boolean;
  119. };
  120. class ContextData extends React.Component<Props, State> {
  121. static defaultProps = {
  122. data: null,
  123. withAnnotatedText: false,
  124. };
  125. renderValue(value: Value) {
  126. const {
  127. preserveQuotes,
  128. meta,
  129. withAnnotatedText,
  130. jsonConsts,
  131. maxDefaultDepth,
  132. } = this.props;
  133. const maxDepth = maxDefaultDepth ?? 2;
  134. function getValueWithAnnotatedText(v: Value, meta?: Meta) {
  135. return <AnnotatedText value={v} meta={meta} />;
  136. }
  137. /*eslint no-shadow:0*/
  138. function walk(value: Value, depth: number) {
  139. let i = 0;
  140. const children: React.ReactNode[] = [];
  141. if (value === null) {
  142. return <span className="val-null">{jsonConsts ? 'null' : 'None'}</span>;
  143. }
  144. if (value === true || value === false) {
  145. return (
  146. <span className="val-bool">
  147. {jsonConsts ? (value ? 'true' : 'false') : value ? 'True' : 'False'}
  148. </span>
  149. );
  150. }
  151. if (isString(value)) {
  152. const valueInfo = analyzeStringForRepr(value);
  153. const valueToBeReturned = withAnnotatedText
  154. ? getValueWithAnnotatedText(valueInfo.repr, meta)
  155. : valueInfo.repr;
  156. const out = [
  157. <span
  158. key="value"
  159. className={
  160. (valueInfo.isString ? 'val-string' : '') +
  161. (valueInfo.isStripped ? ' val-stripped' : '') +
  162. (valueInfo.isMultiLine ? ' val-string-multiline' : '')
  163. }
  164. >
  165. {preserveQuotes ? `"${valueToBeReturned}"` : valueToBeReturned}
  166. </span>,
  167. ];
  168. if (valueInfo.isString && isUrl(value)) {
  169. out.push(
  170. <ExternalLink key="external" href={value} className="external-icon">
  171. <StyledIconOpen size="xs" />
  172. </ExternalLink>
  173. );
  174. }
  175. return out;
  176. }
  177. if (isNumber(value)) {
  178. const valueToBeReturned =
  179. withAnnotatedText && meta ? getValueWithAnnotatedText(value, meta) : value;
  180. return <span>{valueToBeReturned}</span>;
  181. }
  182. if (isArray(value)) {
  183. for (i = 0; i < value.length; i++) {
  184. children.push(
  185. <span className="val-array-item" key={i}>
  186. {walk(value[i], depth + 1)}
  187. {i < value.length - 1 ? (
  188. <span className="val-array-sep">{', '}</span>
  189. ) : null}
  190. </span>
  191. );
  192. }
  193. return (
  194. <span className="val-array">
  195. <span className="val-array-marker">{'['}</span>
  196. <ToggleWrap highUp={depth <= maxDepth} wrapClassName="val-array-items">
  197. {children}
  198. </ToggleWrap>
  199. <span className="val-array-marker">{']'}</span>
  200. </span>
  201. );
  202. }
  203. if (React.isValidElement(value)) {
  204. return value;
  205. }
  206. const keys = Object.keys(value);
  207. keys.sort(naturalCaseInsensitiveSort);
  208. for (i = 0; i < keys.length; i++) {
  209. const key = keys[i];
  210. children.push(
  211. <span className="val-dict-pair" key={key}>
  212. <span className="val-dict-key">
  213. <span className="val-string">{preserveQuotes ? `"${key}"` : key}</span>
  214. </span>
  215. <span className="val-dict-col">{': '}</span>
  216. <span className="val-dict-value">
  217. {walk(value[key], depth + 1)}
  218. {i < keys.length - 1 ? <span className="val-dict-sep">{', '}</span> : null}
  219. </span>
  220. </span>
  221. );
  222. }
  223. return (
  224. <span className="val-dict">
  225. <span className="val-dict-marker">{'{'}</span>
  226. <ToggleWrap highUp={depth <= maxDepth - 1} wrapClassName="val-dict-items">
  227. {children}
  228. </ToggleWrap>
  229. <span className="val-dict-marker">{'}'}</span>
  230. </span>
  231. );
  232. }
  233. return walk(value, 0);
  234. }
  235. render() {
  236. const {
  237. data,
  238. preserveQuotes: _preserveQuotes,
  239. withAnnotatedText: _withAnnotatedText,
  240. meta: _meta,
  241. children,
  242. ...other
  243. } = this.props;
  244. return (
  245. <pre {...other}>
  246. {this.renderValue(data)}
  247. {children}
  248. </pre>
  249. );
  250. }
  251. }
  252. const StyledIconOpen = styled(IconOpen)`
  253. position: relative;
  254. top: 1px;
  255. `;
  256. const ToggleIcon = styled('a')<{isOpen?: boolean}>`
  257. display: inline-block;
  258. position: relative;
  259. top: 1px;
  260. height: 11px;
  261. width: 11px;
  262. line-height: 1;
  263. padding-left: 1px;
  264. margin-left: 1px;
  265. border-radius: 2px;
  266. background: ${p => (p.isOpen ? p.theme.gray300 : p.theme.blue300)};
  267. &:hover {
  268. background: ${p => (p.isOpen ? p.theme.gray400 : p.theme.blue200)};
  269. }
  270. `;
  271. export default ContextData;