123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import {Fragment} from 'react';
- import isPropValid from '@emotion/is-prop-valid';
- import styled from '@emotion/styled';
- import {LocationDescriptor} from 'history';
- import {TagSegment} from 'sentry/actionCreators/events';
- import Link from 'sentry/components/links/link';
- import Tooltip from 'sentry/components/tooltip';
- import Version from 'sentry/components/version';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {percent} from 'sentry/utils';
- type Props = {
- segments: TagSegment[];
- title: string;
- totalValues: number;
- hasError?: boolean;
- isLoading?: boolean;
- onTagClick?: (title: string, value: TagSegment) => void;
- renderEmpty?: () => React.ReactNode;
- renderError?: () => React.ReactNode;
- renderLoading?: () => React.ReactNode;
- showReleasePackage?: boolean;
- };
- type SegmentValue = {
- index: number;
- onClick: () => void;
- to: LocationDescriptor;
- };
- function TagDistributionMeter({
- isLoading = false,
- hasError = false,
- renderLoading = () => null,
- renderEmpty = () => <p>{t('No recent data.')}</p>,
- renderError = () => null,
- showReleasePackage = false,
- segments,
- title,
- totalValues,
- onTagClick,
- }: Props) {
- function renderTitle() {
- if (!Array.isArray(segments) || segments.length <= 0) {
- return (
- <Title>
- <TitleType>{title}</TitleType>
- </Title>
- );
- }
- const largestSegment = segments[0];
- const pct = percent(largestSegment.count, totalValues);
- const pctLabel = Math.floor(pct);
- const renderLabel = () => {
- switch (title) {
- case 'release':
- return (
- <Label>
- <Version
- version={largestSegment.name}
- anchor={false}
- tooltipRawVersion
- withPackage={showReleasePackage}
- truncate
- />
- </Label>
- );
- default:
- return <Label>{largestSegment.name || t('n/a')}</Label>;
- }
- };
- return (
- <Title>
- <TitleType>{title}</TitleType>
- <TitleDescription>
- {renderLabel()}
- {isLoading || hasError ? null : <Percent>{pctLabel}%</Percent>}
- </TitleDescription>
- </Title>
- );
- }
- function renderSegments() {
- if (isLoading) {
- return renderLoading();
- }
- if (hasError) {
- return <SegmentBar>{renderError()}</SegmentBar>;
- }
- if (totalValues === 0) {
- return <SegmentBar>{renderEmpty()}</SegmentBar>;
- }
- return (
- <SegmentBar>
- {segments.map((value, index) => {
- const pct = percent(value.count, totalValues);
- const pctLabel = Math.floor(pct);
- const renderTooltipValue = () => {
- switch (title) {
- case 'release':
- return (
- <Version
- version={value.name}
- anchor={false}
- withPackage={showReleasePackage}
- />
- );
- default:
- return value.name || t('n/a');
- }
- };
- const tooltipHtml = (
- <Fragment>
- <div className="truncate">{renderTooltipValue()}</div>
- {pctLabel}%
- </Fragment>
- );
- const segmentProps: SegmentValue = {
- index,
- to: value.url,
- onClick: () => onTagClick?.(title, value),
- };
- return (
- <div
- data-test-id={`tag-${title}-segment-${value.value}`}
- key={value.value}
- style={{width: pct + '%'}}
- >
- <Tooltip title={tooltipHtml} containerDisplayMode="block">
- {value.isOther ? (
- <OtherSegment aria-label={t('Other')} />
- ) : (
- <Segment
- aria-label={t(
- 'Add the %s %s segment tag to the search query',
- title,
- value.value
- )}
- {...segmentProps}
- />
- )}
- </Tooltip>
- </div>
- );
- })}
- </SegmentBar>
- );
- }
- const totalVisible = segments.reduce((sum, value) => sum + value.count, 0);
- const hasOther = totalVisible < totalValues;
- if (hasOther) {
- segments.push({
- isOther: true,
- name: t('Other'),
- value: 'other',
- count: totalValues - totalVisible,
- url: '',
- });
- }
- return (
- <TagSummary>
- {renderTitle()}
- {renderSegments()}
- </TagSummary>
- );
- }
- export default TagDistributionMeter;
- const COLORS = [
- '#3A3387',
- '#5F40A3',
- '#8C4FBD',
- '#B961D3',
- '#DE76E4',
- '#EF91E8',
- '#F7B2EC',
- '#FCD8F4',
- '#FEEBF9',
- ];
- const TagSummary = styled('div')`
- margin-bottom: ${space(1)};
- `;
- const SegmentBar = styled('div')`
- display: flex;
- overflow: hidden;
- border-radius: ${p => p.theme.borderRadius};
- `;
- const Title = styled('div')`
- display: flex;
- font-size: ${p => p.theme.fontSizeSmall};
- justify-content: space-between;
- margin-bottom: ${space(0.25)};
- line-height: 1.1;
- `;
- const TitleType = styled('div')`
- color: ${p => p.theme.textColor};
- font-weight: bold;
- ${p => p.theme.overflowEllipsis};
- `;
- const TitleDescription = styled('div')`
- display: flex;
- color: ${p => p.theme.gray300};
- text-align: right;
- `;
- const Label = styled('div')`
- ${p => p.theme.overflowEllipsis};
- max-width: 150px;
- `;
- const Percent = styled('div')`
- font-weight: bold;
- font-variant-numeric: tabular-nums;
- padding-left: ${space(0.5)};
- color: ${p => p.theme.textColor};
- `;
- const OtherSegment = styled('span')`
- display: block;
- width: 100%;
- height: 16px;
- color: inherit;
- outline: none;
- background-color: ${COLORS[COLORS.length - 1]};
- `;
- const Segment = styled(Link, {shouldForwardProp: isPropValid})<SegmentValue>`
- display: block;
- width: 100%;
- height: 16px;
- color: inherit;
- outline: none;
- background-color: ${p => COLORS[p.index]};
- border-radius: 0;
- `;
|