import {css, type Theme} from '@emotion/react'; import Color from 'color'; import type {DurationDisplay} from 'sentry/components/performance/waterfall/types'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {space} from 'sentry/styles/space'; import type {SpanBarType} from './constants'; import {getSpanBarColors} from './constants'; export const getBackgroundColor = ({ showStriping, showDetail, theme, }: { theme: Theme; showDetail?: boolean; showStriping?: boolean; }) => { if (showDetail) { return theme.textColor; } if (showStriping) { return theme.backgroundSecondary; } return theme.background; }; export function getHatchPattern(spanBarType: SpanBarType | undefined, theme: Theme) { if (spanBarType) { const {primary, alternate} = getSpanBarColors(spanBarType, theme); return css` background-image: linear-gradient( 135deg, ${alternate}, ${alternate} 2.5px, ${primary} 2.5px, ${primary} 5px, ${alternate} 6px, ${alternate} 8px, ${primary} 8px, ${primary} 11px, ${alternate} 11px, ${alternate} 14px, ${primary} 14px, ${primary} 16.5px, ${alternate} 16.5px, ${alternate} 19px, ${primary} 20px ); background-size: 16px 16px; `; } return null; } export const getDurationPillAlignment = ({ durationDisplay, }: { durationDisplay: DurationDisplay; theme: Theme; spanBarType?: SpanBarType; }) => { switch (durationDisplay) { case 'left': return css` right: calc(100% + ${space(0.5)}); `; case 'right': return css` left: calc(100% + ${space(0.75)}); `; default: return css` right: ${space(0.75)}; `; } }; export const getDurationPillColors = ({ durationDisplay, theme, showDetail, spanBarType, }: { durationDisplay: DurationDisplay; showDetail: boolean; theme: Theme; spanBarType?: SpanBarType; }) => { if (durationDisplay === 'inset') { const {alternate, insetTextColor} = getSpanBarColors(spanBarType, theme); return `background: ${alternate}; color: ${insetTextColor};`; } return `color: ${showDetail ? theme.gray200 : theme.gray300};`; }; export const getToggleTheme = ({ theme, isExpanded, disabled, errored, isSpanGroupToggler, spanBarType, }: { disabled: boolean; errored: boolean; isExpanded: boolean; theme: Theme; isSpanGroupToggler?: boolean; spanBarType?: SpanBarType; }) => { if (spanBarType) { const {primary} = getSpanBarColors(spanBarType, theme); return css` background: ${primary}; border: 2px solid ${theme.button.default.border}; color: ${theme.button.primary.color}; cursor: pointer; `; } const buttonTheme = isExpanded ? theme.button.default : theme.button.primary; const errorTheme = theme.button.danger; const background = errored ? isExpanded ? buttonTheme.background : errorTheme.background : buttonTheme.background; const border = errored ? errorTheme.background : buttonTheme.border; const color = errored ? isExpanded ? errorTheme.background : buttonTheme.color : buttonTheme.color; if (isSpanGroupToggler) { return css` background: ${theme.blue300}; border: 2px solid ${theme.button.default.border}; color: ${color}; cursor: pointer; `; } if (disabled) { return css` background: ${background}; border: 2px solid ${border}; color: ${color}; cursor: default; `; } return css` background: ${background}; border: 2px solid ${border}; color: ${color}; `; }; export const getDurationDisplay = ({ width, left, }: { left: undefined | number; width: undefined | number; }): DurationDisplay => { const spaceNeeded = 0.3; if (left === undefined || width === undefined) { return 'inset'; } if (left + width < 1 - spaceNeeded) { return 'right'; } if (left > spaceNeeded) { return 'left'; } return 'inset'; }; export const getHumanDuration = (duration: number): string => { // note: duration is assumed to be in seconds const durationMs = duration * 1000; return `${durationMs.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, })}ms`; }; type Rect = { height: number; width: number; // x and y are left/top coords respectively x: number; y: number; }; // get position of element relative to top/left of document export const getOffsetOfElement = (element: Element) => { // left and top are relative to viewport const {left, top} = element.getBoundingClientRect(); // get values that the document is currently scrolled by const scrollLeft = window.pageXOffset; const scrollTop = window.pageYOffset; return {x: left + scrollLeft, y: top + scrollTop}; }; export const rectOfContent = (element: Element): Rect => { const {x, y} = getOffsetOfElement(element); // offsets for the border and any scrollbars (clientLeft and clientTop), // and if the element was scrolled (scrollLeft and scrollTop) // // NOTE: clientLeft and clientTop does not account for any margins nor padding const contentOffsetLeft = element.clientLeft - element.scrollLeft; const contentOffsetTop = element.clientTop - element.scrollTop; return { x: x + contentOffsetLeft, y: y + contentOffsetTop, width: element.scrollWidth, height: element.scrollHeight, }; }; const getLetterIndex = (letter: string): number => { const index = 'abcdefghijklmnopqrstuvwxyz'.indexOf(letter) || 0; return index === -1 ? 0 : index; }; const colorsAsArray = Object.keys(CHART_PALETTE).map(key => CHART_PALETTE[17][key]); export const barColors = { default: CHART_PALETTE[17][4], transaction: CHART_PALETTE[17][8], http: CHART_PALETTE[17][10], db: CHART_PALETTE[17][17], }; export const pickBarColor = (input: string | undefined): string => { // We pick the color for span bars using the first three letters of the op name. // That way colors stay consistent between transactions. if (!input || input.length < 3) { return CHART_PALETTE[17][4]; } if (barColors[input]) { return barColors[input]; } const letterIndex1 = getLetterIndex(input[0]!); const letterIndex2 = getLetterIndex(input[1]!); const letterIndex3 = getLetterIndex(input[2]!); const letterIndex4 = getLetterIndex(input[3]!); return colorsAsArray[ (letterIndex1 + letterIndex2 + letterIndex3 + letterIndex4) % colorsAsArray.length ]; }; export const lightenBarColor = ( input: string | undefined, lightenRatio: number ): string => { const barColor = pickBarColor(input); return Color(barColor).lighten(lightenRatio).string(); };