utils.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import type {Query} from 'history';
  2. import type {EventTag} from 'sentry/types/event';
  3. import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/formatters';
  4. import {appendTagCondition} from 'sentry/utils/queryString';
  5. function arrayIsEqual(arr?: any[], other?: any[], deep?: boolean): boolean {
  6. // if the other array is a falsy value, return
  7. if (!arr && !other) {
  8. return true;
  9. }
  10. if (!arr || !other) {
  11. return false;
  12. }
  13. // compare lengths - can save a lot of time
  14. if (arr.length !== other.length) {
  15. return false;
  16. }
  17. return arr.every((val, idx) => valueIsEqual(val, other[idx], deep));
  18. }
  19. export function valueIsEqual(value?: any, other?: any, deep?: boolean): boolean {
  20. if (value === other) {
  21. return true;
  22. }
  23. if (Array.isArray(value) || Array.isArray(other)) {
  24. if (arrayIsEqual(value, other, deep)) {
  25. return true;
  26. }
  27. } else if (
  28. (value && typeof value === 'object') ||
  29. (other && typeof other === 'object')
  30. ) {
  31. if (objectMatchesSubset(value, other, deep)) {
  32. return true;
  33. }
  34. }
  35. return false;
  36. }
  37. function objectMatchesSubset(obj?: object, other?: object, deep?: boolean): boolean {
  38. let k: string;
  39. if (obj === other) {
  40. return true;
  41. }
  42. if (!obj || !other) {
  43. return false;
  44. }
  45. if (deep !== true) {
  46. for (k in other) {
  47. if (obj[k] !== other[k]) {
  48. return false;
  49. }
  50. }
  51. return true;
  52. }
  53. for (k in other) {
  54. if (!valueIsEqual(obj[k], other[k], deep)) {
  55. return false;
  56. }
  57. }
  58. return true;
  59. }
  60. export function intcomma(x: number): string {
  61. return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  62. }
  63. /**
  64. * Replaces slug special chars with a space
  65. */
  66. export function explodeSlug(slug: string): string {
  67. return slug.replace(/[-_]+/g, ' ').trim();
  68. }
  69. export function defined<T>(item: T): item is Exclude<T, null | undefined> {
  70. return item !== undefined && item !== null;
  71. }
  72. export function nl2br(str: string): string {
  73. return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
  74. }
  75. export function escape(str: string): string {
  76. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  77. }
  78. export function percent(value: number, totalValue: number): number {
  79. // prevent division by zero
  80. if (totalValue === 0) {
  81. return 0;
  82. }
  83. return (value / totalValue) * 100;
  84. }
  85. /**
  86. * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
  87. * means that:
  88. * - 1000 megabytes is equal to 1 gigabyte
  89. * - 1024 mebibytes is equal to 1 gibibytes
  90. *
  91. * We will use base 10 throughout billing for attachments. This function formats
  92. * quota/usage values for display.
  93. *
  94. * For storage/memory/file sizes, please take a look at formatBytesBase2
  95. */
  96. export function formatBytesBase10(bytes: number, u: number = 0) {
  97. const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  98. const threshold = 1000;
  99. while (bytes >= threshold) {
  100. bytes /= threshold;
  101. u += 1;
  102. }
  103. return formatNumberWithDynamicDecimalPoints(bytes) + ' ' + units[u];
  104. }
  105. /**
  106. * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
  107. * means that:
  108. * - 1000 megabytes is equal to 1 gigabyte
  109. * - 1024 mebibytes is equal to 1 gibibytes
  110. *
  111. * We will use base 2 to display storage/memory/file sizes as that is commonly
  112. * used by Windows or RAM or CPU cache sizes, and it is more familiar to the user
  113. *
  114. * For billing-related code around attachments. please take a look at
  115. * formatBytesBase10
  116. */
  117. export function formatBytesBase2(bytes: number, fixPoints: number | false = 1): string {
  118. const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
  119. const thresh = 1024;
  120. if (bytes < thresh) {
  121. return (
  122. (fixPoints === false
  123. ? formatNumberWithDynamicDecimalPoints(bytes)
  124. : bytes.toFixed(fixPoints)) + ' B'
  125. );
  126. }
  127. let u = -1;
  128. do {
  129. bytes /= thresh;
  130. ++u;
  131. } while (bytes >= thresh);
  132. return (
  133. (fixPoints === false
  134. ? formatNumberWithDynamicDecimalPoints(bytes)
  135. : bytes.toFixed(fixPoints)) +
  136. ' ' +
  137. units[u]
  138. );
  139. }
  140. export function getShortCommitHash(hash: string): string {
  141. if (hash.match(/^[a-f0-9]{40}$/)) {
  142. hash = hash.substring(0, 7);
  143. }
  144. return hash;
  145. }
  146. export function parseRepo<T>(repo: T): T {
  147. if (typeof repo === 'string') {
  148. const re = /(?:github\.com|bitbucket\.org)\/([^\/]+\/[^\/]+)/i;
  149. const match = repo.match(re);
  150. const parsedRepo = match ? match[1] : repo;
  151. return parsedRepo as any;
  152. }
  153. return repo;
  154. }
  155. /**
  156. * Converts a multi-line textarea input value into an array,
  157. * eliminating empty lines
  158. */
  159. export function extractMultilineFields(value: string): string[] {
  160. return value
  161. .split('\n')
  162. .map(f => f.trim())
  163. .filter(f => f !== '');
  164. }
  165. /**
  166. * If the value is of type Array, converts it to type string, keeping the line breaks, if there is any
  167. */
  168. export function convertMultilineFieldValue<T extends string | string[]>(
  169. value: T
  170. ): string {
  171. if (Array.isArray(value)) {
  172. return value.join('\n');
  173. }
  174. if (typeof value === 'string') {
  175. return value.split('\n').join('\n');
  176. }
  177. return '';
  178. }
  179. // build actorIds
  180. export const buildUserId = (id: string) => `user:${id}`;
  181. export const buildTeamId = (id: string) => `team:${id}`;
  182. /**
  183. * Removes the organization / project scope prefix on feature names.
  184. */
  185. export function descopeFeatureName<T>(feature: T): T | string {
  186. if (typeof feature !== 'string') {
  187. return feature;
  188. }
  189. const results = feature.match(/(?:^(?:projects|organizations):)?(.*)/);
  190. if (results && results.length > 0) {
  191. return results.pop()!;
  192. }
  193. return feature;
  194. }
  195. export function isWebpackChunkLoadingError(error: Error): boolean {
  196. return (
  197. error &&
  198. typeof error.message === 'string' &&
  199. error.message.toLowerCase().includes('loading chunk')
  200. );
  201. }
  202. export function generateQueryWithTag(prevQuery: Query, tag: EventTag): Query {
  203. const query = {...prevQuery};
  204. // some tags are dedicated query strings since other parts of the app consumes this,
  205. // for example, the global selection header.
  206. switch (tag.key) {
  207. case 'environment':
  208. query.environment = tag.value;
  209. break;
  210. case 'project':
  211. query.project = tag.value;
  212. break;
  213. default:
  214. query.query = appendTagCondition(query.query, tag.key, tag.value);
  215. }
  216. return query;
  217. }
  218. // NOTE: only escapes a " if it's not already escaped
  219. export function escapeDoubleQuotes(str: string) {
  220. return str.replace(/\\([\s\S])|(")/g, '\\$1$2');
  221. }
  222. export function generateOrgSlugUrl(orgSlug) {
  223. const sentryDomain = window.__initialData.links.sentryUrl.split('/')[2];
  224. return `${window.location.protocol}//${orgSlug}.${sentryDomain}${window.location.pathname}`;
  225. }