errorItem.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import moment from 'moment';
  5. import Button from 'app/components/button';
  6. import KeyValueList from 'app/components/events/interfaces/keyValueList';
  7. import {getMeta} from 'app/components/events/meta/metaProxy';
  8. import ListItem from 'app/components/list/listItem';
  9. import {t} from 'app/locale';
  10. import space from 'app/styles/space';
  11. type Error = {
  12. type: string;
  13. message: React.ReactNode;
  14. data?: {
  15. name?: string;
  16. message?: string;
  17. image_path?: string;
  18. image_name?: string;
  19. server_time?: string;
  20. sdk_time?: string;
  21. url?: string;
  22. } & Record<string, any>;
  23. };
  24. const keyMapping = {
  25. image_uuid: 'Debug ID',
  26. image_name: 'File Name',
  27. image_path: 'File Path',
  28. };
  29. type Props = {
  30. error: Error;
  31. };
  32. type State = {
  33. isOpen: boolean;
  34. };
  35. class ErrorItem extends React.Component<Props, State> {
  36. state: State = {
  37. isOpen: false,
  38. };
  39. shouldComponentUpdate(_nextProps: Props, nextState: State) {
  40. return this.state.isOpen !== nextState.isOpen;
  41. }
  42. handleToggle = () => {
  43. this.setState({isOpen: !this.state.isOpen});
  44. };
  45. cleanedData(errorData: NonNullable<Error['data']>) {
  46. const data = {...errorData};
  47. // The name is rendered as path in front of the message
  48. if (typeof data.name === 'string') {
  49. delete data.name;
  50. }
  51. if (data.message === 'None') {
  52. // Python ensures a message string, but "None" doesn't make sense here
  53. delete data.message;
  54. }
  55. if (typeof data.image_path === 'string') {
  56. // Separate the image name for readability
  57. const separator = /^([a-z]:\\|\\\\)/i.test(data.image_path) ? '\\' : '/';
  58. const path = data.image_path.split(separator);
  59. data.image_name = path.splice(-1, 1)[0];
  60. data.image_path = path.length ? path.join(separator) + separator : '';
  61. }
  62. if (typeof data.server_time === 'string' && typeof data.sdk_time === 'string') {
  63. data.message = t(
  64. 'Adjusted timestamps by %s',
  65. moment
  66. .duration(moment.utc(data.server_time).diff(moment.utc(data.sdk_time)))
  67. .humanize()
  68. );
  69. }
  70. return Object.entries(data).map(([key, value]) => ({
  71. key,
  72. value,
  73. subject: keyMapping[key] || startCase(key),
  74. meta: getMeta(data, key),
  75. }));
  76. }
  77. renderPath(data: NonNullable<Error['data']>) {
  78. const {name} = data;
  79. if (!name || typeof name !== 'string') {
  80. return null;
  81. }
  82. return (
  83. <React.Fragment>
  84. <strong>{name}</strong>
  85. {': '}
  86. </React.Fragment>
  87. );
  88. }
  89. render() {
  90. const {error} = this.props;
  91. const {isOpen} = this.state;
  92. const data = error?.data ?? {};
  93. const cleanedData = this.cleanedData(data);
  94. return (
  95. <StyledListItem>
  96. <OverallInfo>
  97. <div>
  98. {this.renderPath(data)}
  99. {error.message}
  100. </div>
  101. {!!cleanedData.length && (
  102. <ToggleButton onClick={this.handleToggle} priority="link">
  103. {isOpen ? t('Collapse') : t('Expand')}
  104. </ToggleButton>
  105. )}
  106. </OverallInfo>
  107. {isOpen && <KeyValueList data={cleanedData} isContextData />}
  108. </StyledListItem>
  109. );
  110. }
  111. }
  112. export default ErrorItem;
  113. const ToggleButton = styled(Button)`
  114. margin-left: ${space(1.5)};
  115. font-weight: 700;
  116. color: ${p => p.theme.subText};
  117. :hover,
  118. :focus {
  119. color: ${p => p.theme.textColor};
  120. }
  121. `;
  122. const StyledListItem = styled(ListItem)`
  123. margin-bottom: ${space(0.75)};
  124. `;
  125. const OverallInfo = styled('div')`
  126. display: grid;
  127. grid-template-columns: repeat(2, minmax(auto, max-content));
  128. word-break: break-all;
  129. `;