utils.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import {Query} from 'history';
  2. import cloneDeep from 'lodash/cloneDeep';
  3. import ConfigStore from 'sentry/stores/configStore';
  4. import {Project} from 'sentry/types';
  5. import {EventTag} from 'sentry/types/event';
  6. import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/formatters';
  7. import {appendTagCondition} from 'sentry/utils/queryString';
  8. function arrayIsEqual(arr?: any[], other?: any[], deep?: boolean): boolean {
  9. // if the other array is a falsy value, return
  10. if (!arr && !other) {
  11. return true;
  12. }
  13. if (!arr || !other) {
  14. return false;
  15. }
  16. // compare lengths - can save a lot of time
  17. if (arr.length !== other.length) {
  18. return false;
  19. }
  20. return arr.every((val, idx) => valueIsEqual(val, other[idx], deep));
  21. }
  22. export function valueIsEqual(value?: any, other?: any, deep?: boolean): boolean {
  23. if (value === other) {
  24. return true;
  25. }
  26. if (Array.isArray(value) || Array.isArray(other)) {
  27. if (arrayIsEqual(value, other, deep)) {
  28. return true;
  29. }
  30. } else if (
  31. (value && typeof value === 'object') ||
  32. (other && typeof other === 'object')
  33. ) {
  34. if (objectMatchesSubset(value, other, deep)) {
  35. return true;
  36. }
  37. }
  38. return false;
  39. }
  40. function objectMatchesSubset(obj?: object, other?: object, deep?: boolean): boolean {
  41. let k: string;
  42. if (obj === other) {
  43. return true;
  44. }
  45. if (!obj || !other) {
  46. return false;
  47. }
  48. if (deep !== true) {
  49. for (k in other) {
  50. if (obj[k] !== other[k]) {
  51. return false;
  52. }
  53. }
  54. return true;
  55. }
  56. for (k in other) {
  57. if (!valueIsEqual(obj[k], other[k], deep)) {
  58. return false;
  59. }
  60. }
  61. return true;
  62. }
  63. export function intcomma(x: number): string {
  64. return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  65. }
  66. export function lastOfArray<T extends Array<unknown> | ReadonlyArray<unknown>>(
  67. t: T
  68. ): T[number] {
  69. return t[t.length - 1];
  70. }
  71. export function sortArray<T>(arr: Array<T>, score_fn: (entry: T) => string): Array<T> {
  72. arr.sort((a, b) => {
  73. const a_score = score_fn(a),
  74. b_score = score_fn(b);
  75. for (let i = 0; i < a_score.length; i++) {
  76. if (a_score[i] > b_score[i]) {
  77. return 1;
  78. }
  79. if (a_score[i] < b_score[i]) {
  80. return -1;
  81. }
  82. }
  83. return 0;
  84. });
  85. return arr;
  86. }
  87. export function objectIsEmpty(obj = {}): boolean {
  88. for (const prop in obj) {
  89. if (obj.hasOwnProperty(prop)) {
  90. return false;
  91. }
  92. }
  93. return true;
  94. }
  95. export function trim(str: string): string {
  96. return str.replace(/^\s+|\s+$/g, '');
  97. }
  98. /**
  99. * Replaces slug special chars with a space
  100. */
  101. export function explodeSlug(slug: string): string {
  102. return trim(slug.replace(/[-_]+/g, ' '));
  103. }
  104. export function defined<T>(item: T): item is Exclude<T, null | undefined> {
  105. return item !== undefined && item !== null;
  106. }
  107. /**
  108. * Omit keys from an object. The return value will be a deep clone of the input,
  109. * meaning none of the references will be preserved. If you require faster shallow cloning,
  110. * use {prop, ...rest} = obj spread syntax instead.
  111. */
  112. // omit<T extends object, K extends PropertyName[]>(
  113. // object: T | null | undefined,
  114. // ...paths: K
  115. // ): Pick<T, Exclude<keyof T, K[number]>>;
  116. export function omit<T extends object, K extends Extract<keyof T, string>>(
  117. obj: T | null | undefined,
  118. key: (K | (string & {}))[] | readonly (K | (string & {}))[]
  119. ): Pick<T, Exclude<keyof T, K[][number]>>;
  120. export function omit<T extends object, K extends Extract<keyof T, string>>(
  121. obj: T | null | undefined,
  122. key: K | (string & {})
  123. ): Pick<T, Exclude<keyof T, K[]>>;
  124. export function omit<T extends object, K extends Extract<keyof T, string>>(
  125. obj: T | null | undefined,
  126. // @TODO: If keys can be statically known, we should provide a ts helper to
  127. // enforce it. I am fairly certain this will not work with generics as we'll
  128. // just end up blowing through the stack recursion, but it could be done on-demand.
  129. keys: (K | (string & {})) | (K | (string & {}))[]
  130. // T return type is wrong, but we cannot statically infer nested keys without
  131. // narrowing the type, which seems impossible for a generic implementation? Because
  132. // of this, allow users to type the return value and not
  133. ) {
  134. if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
  135. // It would have been more correct to throw and error, however
  136. // the lodash implementation we were using before did not do that
  137. // and we have a lot of code that relies on this behavior.
  138. return {};
  139. }
  140. let returnValue: T | undefined;
  141. // We need to feature detect structured cloning instead of just
  142. // wrapping it inside a try/catch as somehow ends up crashing
  143. // our selenium webdriver tests...
  144. if (typeof window !== 'undefined' && 'structuredClone' in window) {
  145. try {
  146. returnValue = window.structuredClone(obj);
  147. } catch {
  148. returnValue = cloneDeep(obj);
  149. }
  150. } else {
  151. returnValue = cloneDeep(obj);
  152. }
  153. if (!returnValue) {
  154. throw new TypeError(`Could not clone object ${JSON.stringify(obj)}`);
  155. }
  156. if (typeof keys === 'string') {
  157. deepRemoveKey(returnValue, keys);
  158. return returnValue;
  159. }
  160. // @TODO: there is an optimization opportunity here. If we presort the keys,
  161. // then we can treat the traversal as a tree and avoid having to traverse the
  162. // entire object for each key. This would be a good idea if we expect to
  163. // omit many deep keys from an object.
  164. for (let i = 0; i < keys.length; i++) {
  165. deepRemoveKey(returnValue, keys[i]);
  166. }
  167. return returnValue;
  168. }
  169. function deepRemoveKey(obj: Record<string, any>, key: string): void {
  170. if (typeof key === 'string') {
  171. if (key in obj) {
  172. delete obj[key];
  173. }
  174. const components = key.split('.');
  175. // < 3 length keys will always be first level keys
  176. // as dot notation requires at least 3 characters
  177. if (key.length < 3 || components.length === 1) {
  178. return;
  179. }
  180. const componentsSize = components.length;
  181. let componentIndex = 0;
  182. let v = obj;
  183. while (componentIndex < componentsSize - 1) {
  184. v = v[components[componentIndex]];
  185. if (v === undefined) {
  186. break;
  187. }
  188. componentIndex++;
  189. }
  190. // will only be defined if we traversed the entire path
  191. if (v !== undefined) {
  192. delete v[components[componentsSize - 1]];
  193. }
  194. }
  195. }
  196. export function nl2br(str: string): string {
  197. return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
  198. }
  199. /**
  200. * This function has a critical security impact, make sure to check all usages before changing this function.
  201. * In some parts of our code we rely on that this only really is a string starting with http(s).
  202. */
  203. export function isUrl(str: any): boolean {
  204. return (
  205. typeof str === 'string' &&
  206. (str.indexOf('http://') === 0 || str.indexOf('https://') === 0)
  207. );
  208. }
  209. export function escape(str: string): string {
  210. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  211. }
  212. export function percent(value: number, totalValue: number): number {
  213. // prevent division by zero
  214. if (totalValue === 0) {
  215. return 0;
  216. }
  217. return (value / totalValue) * 100;
  218. }
  219. export function toTitleCase(str: string): string {
  220. return str.replace(
  221. /\w\S*/g,
  222. txt => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()
  223. );
  224. }
  225. /**
  226. * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
  227. * means that:
  228. * - 1000 megabytes is equal to 1 gigabyte
  229. * - 1024 mebibytes is equal to 1 gibibytes
  230. *
  231. * We will use base 10 throughout billing for attachments. This function formats
  232. * quota/usage values for display.
  233. *
  234. * For storage/memory/file sizes, please take a look at formatBytesBase2
  235. */
  236. export function formatBytesBase10(bytes: number, u: number = 0) {
  237. const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  238. const threshold = 1000;
  239. while (bytes >= threshold) {
  240. bytes /= threshold;
  241. u += 1;
  242. }
  243. return formatNumberWithDynamicDecimalPoints(bytes) + ' ' + units[u];
  244. }
  245. /**
  246. * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
  247. * means that:
  248. * - 1000 megabytes is equal to 1 gigabyte
  249. * - 1024 mebibytes is equal to 1 gibibytes
  250. *
  251. * We will use base 2 to display storage/memory/file sizes as that is commonly
  252. * used by Windows or RAM or CPU cache sizes, and it is more familiar to the user
  253. *
  254. * For billing-related code around attachments. please take a look at
  255. * formatBytesBase10
  256. */
  257. export function formatBytesBase2(bytes: number, fixPoints: number | false = 1): string {
  258. const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
  259. const thresh = 1024;
  260. if (bytes < thresh) {
  261. return (
  262. (fixPoints === false
  263. ? formatNumberWithDynamicDecimalPoints(bytes)
  264. : bytes.toFixed(fixPoints)) + ' B'
  265. );
  266. }
  267. let u = -1;
  268. do {
  269. bytes /= thresh;
  270. ++u;
  271. } while (bytes >= thresh);
  272. return (
  273. (fixPoints === false
  274. ? formatNumberWithDynamicDecimalPoints(bytes)
  275. : bytes.toFixed(fixPoints)) +
  276. ' ' +
  277. units[u]
  278. );
  279. }
  280. export function getShortCommitHash(hash: string): string {
  281. if (hash.match(/^[a-f0-9]{40}$/)) {
  282. hash = hash.substring(0, 7);
  283. }
  284. return hash;
  285. }
  286. export function parseRepo<T>(repo: T): T {
  287. if (typeof repo === 'string') {
  288. const re = /(?:github\.com|bitbucket\.org)\/([^\/]+\/[^\/]+)/i;
  289. const match = repo.match(re);
  290. const parsedRepo = match ? match[1] : repo;
  291. return parsedRepo as any;
  292. }
  293. return repo;
  294. }
  295. /**
  296. * Converts a multi-line textarea input value into an array,
  297. * eliminating empty lines
  298. */
  299. export function extractMultilineFields(value: string): string[] {
  300. return value
  301. .split('\n')
  302. .map(f => trim(f))
  303. .filter(f => f !== '');
  304. }
  305. /**
  306. * If the value is of type Array, converts it to type string, keeping the line breaks, if there is any
  307. */
  308. export function convertMultilineFieldValue<T extends string | string[]>(
  309. value: T
  310. ): string {
  311. if (Array.isArray(value)) {
  312. return value.join('\n');
  313. }
  314. if (typeof value === 'string') {
  315. return value.split('\n').join('\n');
  316. }
  317. return '';
  318. }
  319. function projectDisplayCompare(a: Project, b: Project): number {
  320. if (a.isBookmarked !== b.isBookmarked) {
  321. return a.isBookmarked ? -1 : 1;
  322. }
  323. return a.slug.localeCompare(b.slug);
  324. }
  325. // Sort a list of projects by bookmarkedness, then by id
  326. export function sortProjects(projects: Array<Project>): Array<Project> {
  327. return projects.sort(projectDisplayCompare);
  328. }
  329. // build actorIds
  330. export const buildUserId = (id: string) => `user:${id}`;
  331. export const buildTeamId = (id: string) => `team:${id}`;
  332. /**
  333. * Removes the organization / project scope prefix on feature names.
  334. */
  335. export function descopeFeatureName<T>(feature: T): T | string {
  336. if (typeof feature !== 'string') {
  337. return feature;
  338. }
  339. const results = feature.match(/(?:^(?:projects|organizations):)?(.*)/);
  340. if (results && results.length > 0) {
  341. return results.pop()!;
  342. }
  343. return feature;
  344. }
  345. export function isWebpackChunkLoadingError(error: Error): boolean {
  346. return (
  347. error &&
  348. typeof error.message === 'string' &&
  349. error.message.toLowerCase().includes('loading chunk')
  350. );
  351. }
  352. export function deepFreeze<T>(object: T) {
  353. // Retrieve the property names defined on object
  354. const propNames = Object.getOwnPropertyNames(object);
  355. // Freeze properties before freezing self
  356. for (const name of propNames) {
  357. const value = object[name];
  358. object[name] = value && typeof value === 'object' ? deepFreeze(value) : value;
  359. }
  360. return Object.freeze(object);
  361. }
  362. export function generateQueryWithTag(prevQuery: Query, tag: EventTag): Query {
  363. const query = {...prevQuery};
  364. // some tags are dedicated query strings since other parts of the app consumes this,
  365. // for example, the global selection header.
  366. switch (tag.key) {
  367. case 'environment':
  368. query.environment = tag.value;
  369. break;
  370. case 'project':
  371. query.project = tag.value;
  372. break;
  373. default:
  374. query.query = appendTagCondition(query.query, tag.key, tag.value);
  375. }
  376. return query;
  377. }
  378. export const isFunction = (value: any): value is Function => typeof value === 'function';
  379. // NOTE: only escapes a " if it's not already escaped
  380. export function escapeDoubleQuotes(str: string) {
  381. return str.replace(/\\([\s\S])|(")/g, '\\$1$2');
  382. }
  383. export function generateBaseControlSiloUrl() {
  384. return ConfigStore.get('links').sentryUrl || '';
  385. }
  386. export function generateOrgSlugUrl(orgSlug) {
  387. const sentryDomain = window.__initialData.links.sentryUrl.split('/')[2];
  388. return `${window.location.protocol}//${orgSlug}.${sentryDomain}${window.location.pathname}`;
  389. }