utils.tsx 8.2 KB

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