utils.tsx 7.1 KB

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