123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- import type {Query} from 'history';
- import ConfigStore from 'sentry/stores/configStore';
- import type {EventTag} from 'sentry/types/event';
- import type {Project} from 'sentry/types/project';
- import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/formatters';
- import {appendTagCondition} from 'sentry/utils/queryString';
- function arrayIsEqual(arr?: any[], other?: any[], deep?: boolean): boolean {
- // if the other array is a falsy value, return
- if (!arr && !other) {
- return true;
- }
- if (!arr || !other) {
- return false;
- }
- // compare lengths - can save a lot of time
- if (arr.length !== other.length) {
- return false;
- }
- return arr.every((val, idx) => valueIsEqual(val, other[idx], deep));
- }
- export function valueIsEqual(value?: any, other?: any, deep?: boolean): boolean {
- if (value === other) {
- return true;
- }
- if (Array.isArray(value) || Array.isArray(other)) {
- if (arrayIsEqual(value, other, deep)) {
- return true;
- }
- } else if (
- (value && typeof value === 'object') ||
- (other && typeof other === 'object')
- ) {
- if (objectMatchesSubset(value, other, deep)) {
- return true;
- }
- }
- return false;
- }
- function objectMatchesSubset(obj?: object, other?: object, deep?: boolean): boolean {
- let k: string;
- if (obj === other) {
- return true;
- }
- if (!obj || !other) {
- return false;
- }
- if (deep !== true) {
- for (k in other) {
- if (obj[k] !== other[k]) {
- return false;
- }
- }
- return true;
- }
- for (k in other) {
- if (!valueIsEqual(obj[k], other[k], deep)) {
- return false;
- }
- }
- return true;
- }
- export function intcomma(x: number): string {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
- }
- export function lastOfArray<T extends Array<unknown> | ReadonlyArray<unknown>>(
- t: T
- ): T[number] {
- return t[t.length - 1];
- }
- export function sortArray<T>(arr: Array<T>, score_fn: (entry: T) => string): Array<T> {
- arr.sort((a, b) => {
- const a_score = score_fn(a),
- b_score = score_fn(b);
- for (let i = 0; i < a_score.length; i++) {
- if (a_score[i] > b_score[i]) {
- return 1;
- }
- if (a_score[i] < b_score[i]) {
- return -1;
- }
- }
- return 0;
- });
- return arr;
- }
- export function objectIsEmpty(obj = {}): boolean {
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- return false;
- }
- }
- return true;
- }
- export function trim(str: string): string {
- return str.replace(/^\s+|\s+$/g, '');
- }
- /**
- * Replaces slug special chars with a space
- */
- export function explodeSlug(slug: string): string {
- return trim(slug.replace(/[-_]+/g, ' '));
- }
- export function defined<T>(item: T): item is Exclude<T, null | undefined> {
- return item !== undefined && item !== null;
- }
- export function nl2br(str: string): string {
- return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
- }
- /**
- * This function has a critical security impact, make sure to check all usages before changing this function.
- * In some parts of our code we rely on that this only really is a string starting with http(s).
- */
- export function isUrl(str: any): boolean {
- return (
- typeof str === 'string' &&
- (str.indexOf('http://') === 0 || str.indexOf('https://') === 0)
- );
- }
- export function escape(str: string): string {
- return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
- }
- export function percent(value: number, totalValue: number): number {
- // prevent division by zero
- if (totalValue === 0) {
- return 0;
- }
- return (value / totalValue) * 100;
- }
- export function toTitleCase(str: string): string {
- return str.replace(
- /\w\S*/g,
- txt => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()
- );
- }
- /**
- * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
- * means that:
- * - 1000 megabytes is equal to 1 gigabyte
- * - 1024 mebibytes is equal to 1 gibibytes
- *
- * We will use base 10 throughout billing for attachments. This function formats
- * quota/usage values for display.
- *
- * For storage/memory/file sizes, please take a look at formatBytesBase2
- */
- export function formatBytesBase10(bytes: number, u: number = 0) {
- const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
- const threshold = 1000;
- while (bytes >= threshold) {
- bytes /= threshold;
- u += 1;
- }
- return formatNumberWithDynamicDecimalPoints(bytes) + ' ' + units[u];
- }
- /**
- * Note the difference between *a-bytes (base 10) vs *i-bytes (base 2), which
- * means that:
- * - 1000 megabytes is equal to 1 gigabyte
- * - 1024 mebibytes is equal to 1 gibibytes
- *
- * We will use base 2 to display storage/memory/file sizes as that is commonly
- * used by Windows or RAM or CPU cache sizes, and it is more familiar to the user
- *
- * For billing-related code around attachments. please take a look at
- * formatBytesBase10
- */
- export function formatBytesBase2(bytes: number, fixPoints: number | false = 1): string {
- const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
- const thresh = 1024;
- if (bytes < thresh) {
- return (
- (fixPoints === false
- ? formatNumberWithDynamicDecimalPoints(bytes)
- : bytes.toFixed(fixPoints)) + ' B'
- );
- }
- let u = -1;
- do {
- bytes /= thresh;
- ++u;
- } while (bytes >= thresh);
- return (
- (fixPoints === false
- ? formatNumberWithDynamicDecimalPoints(bytes)
- : bytes.toFixed(fixPoints)) +
- ' ' +
- units[u]
- );
- }
- export function getShortCommitHash(hash: string): string {
- if (hash.match(/^[a-f0-9]{40}$/)) {
- hash = hash.substring(0, 7);
- }
- return hash;
- }
- export function parseRepo<T>(repo: T): T {
- if (typeof repo === 'string') {
- const re = /(?:github\.com|bitbucket\.org)\/([^\/]+\/[^\/]+)/i;
- const match = repo.match(re);
- const parsedRepo = match ? match[1] : repo;
- return parsedRepo as any;
- }
- return repo;
- }
- /**
- * Converts a multi-line textarea input value into an array,
- * eliminating empty lines
- */
- export function extractMultilineFields(value: string): string[] {
- return value
- .split('\n')
- .map(f => trim(f))
- .filter(f => f !== '');
- }
- /**
- * If the value is of type Array, converts it to type string, keeping the line breaks, if there is any
- */
- export function convertMultilineFieldValue<T extends string | string[]>(
- value: T
- ): string {
- if (Array.isArray(value)) {
- return value.join('\n');
- }
- if (typeof value === 'string') {
- return value.split('\n').join('\n');
- }
- return '';
- }
- function projectDisplayCompare(a: Project, b: Project): number {
- if (a.isBookmarked !== b.isBookmarked) {
- return a.isBookmarked ? -1 : 1;
- }
- return a.slug.localeCompare(b.slug);
- }
- // Sort a list of projects by bookmarkedness, then by id
- export function sortProjects(projects: Array<Project>): Array<Project> {
- return projects.sort(projectDisplayCompare);
- }
- // build actorIds
- export const buildUserId = (id: string) => `user:${id}`;
- export const buildTeamId = (id: string) => `team:${id}`;
- /**
- * Removes the organization / project scope prefix on feature names.
- */
- export function descopeFeatureName<T>(feature: T): T | string {
- if (typeof feature !== 'string') {
- return feature;
- }
- const results = feature.match(/(?:^(?:projects|organizations):)?(.*)/);
- if (results && results.length > 0) {
- return results.pop()!;
- }
- return feature;
- }
- export function isWebpackChunkLoadingError(error: Error): boolean {
- return (
- error &&
- typeof error.message === 'string' &&
- error.message.toLowerCase().includes('loading chunk')
- );
- }
- export function deepFreeze<T>(object: T) {
- // Retrieve the property names defined on object
- const propNames = Object.getOwnPropertyNames(object);
- // Freeze properties before freezing self
- for (const name of propNames) {
- const value = object[name];
- object[name] = value && typeof value === 'object' ? deepFreeze(value) : value;
- }
- return Object.freeze(object);
- }
- export function generateQueryWithTag(prevQuery: Query, tag: EventTag): Query {
- const query = {...prevQuery};
- // some tags are dedicated query strings since other parts of the app consumes this,
- // for example, the global selection header.
- switch (tag.key) {
- case 'environment':
- query.environment = tag.value;
- break;
- case 'project':
- query.project = tag.value;
- break;
- default:
- query.query = appendTagCondition(query.query, tag.key, tag.value);
- }
- return query;
- }
- export const isFunction = (value: any): value is Function => typeof value === 'function';
- // NOTE: only escapes a " if it's not already escaped
- export function escapeDoubleQuotes(str: string) {
- return str.replace(/\\([\s\S])|(")/g, '\\$1$2');
- }
- export function generateBaseControlSiloUrl() {
- return ConfigStore.get('links').sentryUrl || '';
- }
- export function generateOrgSlugUrl(orgSlug) {
- const sentryDomain = window.__initialData.links.sentryUrl.split('/')[2];
- return `${window.location.protocol}//${orgSlug}.${sentryDomain}${window.location.pathname}`;
- }
|