utils.tsx 8.9 KB

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