feat(stats): make rowcount more human readable (#8232)

This commit is contained in:
Joshua Eilers 2023-06-14 10:54:19 -07:00 committed by GitHub
parent c5cc53b99a
commit 071ef4d111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 40 deletions

View File

@ -6,6 +6,8 @@ import { formatNumberWithoutAbbreviation } from '../../../shared/formatNumber';
import { ANTD_GRAY } from '../../shared/constants';
import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils';
import { StatsSummary } from '../../shared/components/styled/StatsSummary';
import { countFormatter, needsFormatting } from '../../../../utils/formatter';
import ExpandingStat from '../../dataset/shared/ExpandingStat';
const StatText = styled.span`
color: ${ANTD_GRAY[8]};
@ -33,9 +35,15 @@ export const ChartStatsSummary = ({
}: Props) => {
const statsViews = [
(!!chartCount && (
<StatText>
<b>{chartCount}</b> charts
</StatText>
<ExpandingStat
disabled={!needsFormatting(chartCount)}
render={(isExpanded) => (
<StatText color={ANTD_GRAY[8]}>
<b>{isExpanded ? formatNumberWithoutAbbreviation(chartCount) : countFormatter(chartCount)}</b>{' '}
charts
</StatText>
)}
/>
)) ||
undefined,
(!!viewCount && (

View File

@ -6,6 +6,8 @@ import { formatNumberWithoutAbbreviation } from '../../../shared/formatNumber';
import { ANTD_GRAY } from '../../shared/constants';
import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils';
import { StatsSummary } from '../../shared/components/styled/StatsSummary';
import { countFormatter, needsFormatting } from '../../../../utils/formatter';
import ExpandingStat from '../../dataset/shared/ExpandingStat';
const StatText = styled.span`
color: ${ANTD_GRAY[8]};
@ -33,9 +35,15 @@ export const DashboardStatsSummary = ({
}: Props) => {
const statsViews = [
(!!chartCount && (
<StatText>
<b>{chartCount}</b> charts
</StatText>
<ExpandingStat
disabled={!needsFormatting(chartCount)}
render={(isExpanded) => (
<StatText color={ANTD_GRAY[8]}>
<b>{isExpanded ? formatNumberWithoutAbbreviation(chartCount) : countFormatter(chartCount)}</b>{' '}
charts
</StatText>
)}
/>
)) ||
undefined,
(!!viewCount && (

View File

@ -7,6 +7,8 @@ import { ANTD_GRAY } from '../../shared/constants';
import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils';
import { StatsSummary } from '../../shared/components/styled/StatsSummary';
import { FormattedBytesStat } from './FormattedBytesStat';
import { countFormatter, needsFormatting } from '../../../../utils/formatter';
import ExpandingStat from './ExpandingStat';
const StatText = styled.span<{ color: string }>`
color: ${(props) => props.color};
@ -25,6 +27,7 @@ type Props = {
uniqueUserCountLast30Days?: number | null;
lastUpdatedMs?: number | null;
color?: string;
mode?: 'normal' | 'tooltip-content';
};
export const DatasetStatsSummary = ({
@ -36,20 +39,33 @@ export const DatasetStatsSummary = ({
uniqueUserCountLast30Days,
lastUpdatedMs,
color,
mode = 'normal',
}: Props) => {
const displayedColor = color !== undefined ? color : ANTD_GRAY[7];
const isTooltipMode = mode === 'tooltip-content';
const displayedColor = isTooltipMode ? '' : color ?? ANTD_GRAY[7];
const statsViews = [
!!rowCount && (
<StatText color={displayedColor}>
<TableOutlined style={{ marginRight: 8, color: displayedColor }} />
<b>{formatNumberWithoutAbbreviation(rowCount)}</b> rows
{!!columnCount && (
<>
, <b>{formatNumberWithoutAbbreviation(columnCount)}</b> columns
</>
<ExpandingStat
disabled={isTooltipMode || !needsFormatting(rowCount)}
render={(isExpanded) => (
<StatText color={displayedColor}>
<TableOutlined style={{ marginRight: 8, color: displayedColor }} />
<b>{isExpanded ? formatNumberWithoutAbbreviation(rowCount) : countFormatter(rowCount)}</b> rows
{!!columnCount && (
<>
,{' '}
<b>
{isExpanded
? formatNumberWithoutAbbreviation(columnCount)
: countFormatter(columnCount)}
</b>{' '}
columns
</>
)}
</StatText>
)}
</StatText>
/>
),
!!sizeInBytes && (
<StatText color={displayedColor}>

View File

@ -0,0 +1,48 @@
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
const ExpandingStatContainer = styled.span<{ disabled: boolean; expanded: boolean; width: string }>`
overflow: hidden;
white-space: nowrap;
width: ${(props) => props.width};
transition: width 250ms ease;
`;
const ExpandingStat = ({
disabled = false,
render,
}: {
disabled?: boolean;
render: (isExpanded: boolean) => ReactNode;
}) => {
const contentRef = useRef<HTMLSpanElement>(null);
const [width, setWidth] = useState<string>('inherit');
const [isExpanded, setIsExpanded] = useState(false);
useEffect(() => {
if (!contentRef.current) return;
setWidth(`${contentRef.current.offsetWidth}px`);
}, [isExpanded]);
const onMouseEnter = () => {
if (!disabled) setIsExpanded(true);
};
const onMouseLeave = () => {
if (!disabled) setIsExpanded(false);
};
return (
<ExpandingStatContainer
disabled={disabled}
expanded={isExpanded}
width={width}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<span ref={contentRef}>{render(isExpanded)}</span>
</ExpandingStatContainer>
);
};
export default ExpandingStat;

View File

@ -8,15 +8,15 @@ type Props = {
const StatsContainer = styled.div`
margin-top: 8px;
display: flex;
align-items: center;
`;
const StatDivider = styled.div`
display: inline-block;
padding-left: 10px;
margin-right: 10px;
border-right: 1px solid ${ANTD_GRAY[4]};
height: 21px;
vertical-align: text-top;
`;
export const StatsSummary = ({ stats }: Props) => {
@ -25,10 +25,10 @@ export const StatsSummary = ({ stats }: Props) => {
{stats && stats.length > 0 && (
<StatsContainer>
{stats.map((statView, index) => (
<span>
<>
{statView}
{index < stats.length - 1 && <StatDivider />}
</span>
</>
))}
</StatsContainer>
)}

View File

@ -8,7 +8,7 @@ import { ANTD_GRAY } from '../../../../constants';
import { useBaseEntity, useRouteToTab } from '../../../../EntityContext';
import { SidebarHeader } from '../SidebarHeader';
import { InfoItem } from '../../../../components/styled/InfoItem';
import { countSeparator } from '../../../../../../../utils/formatter/index';
import { formatNumberWithoutAbbreviation } from '../../../../../../shared/formatNumber';
const HeaderInfoBody = styled(Typography.Text)`
font-size: 16px;
@ -83,7 +83,7 @@ export const SidebarStatsSection = () => {
onClick={() => routeToTab({ tabName: 'Queries' })}
width={INFO_ITEM_WIDTH_PX}
>
<HeaderInfoBody>{countSeparator(latestProfile?.rowCount)}</HeaderInfoBody>
<HeaderInfoBody>{formatNumberWithoutAbbreviation(latestProfile?.rowCount)}</HeaderInfoBody>
</InfoItem>
) : null}
{latestProfile?.columnCount ? (

View File

@ -4,8 +4,9 @@ import styled from 'styled-components';
import { CorpUser, Maybe, UserUsageCounts } from '../../../../../../../types.generated';
import { InfoItem } from '../../../../components/styled/InfoItem';
import { ANTD_GRAY } from '../../../../constants';
import { countFormatter, countSeparator } from '../../../../../../../utils/formatter/index';
import { countFormatter } from '../../../../../../../utils/formatter/index';
import { ExpandedActorGroup } from '../../../../components/styled/ExpandedActorGroup';
import { formatNumberWithoutAbbreviation } from '../../../../../../shared/formatNumber';
type Props = {
rowCount?: number;
@ -57,7 +58,7 @@ export default function TableStats({
<StatContainer justifyContent={justifyContent}>
{rowCount && (
<InfoItem title="Rows">
<Tooltip title={countSeparator(rowCount)} placement="right">
<Tooltip title={formatNumberWithoutAbbreviation(rowCount)} placement="right">
<Typography.Text strong style={{ fontSize: 24 }} data-testid="table-stats-rowcount">
{countFormatter(rowCount)}
</Typography.Text>

View File

@ -39,7 +39,7 @@ export default function AutoCompleteItem({ query, entity }: Props) {
return (
<Tooltip
overlayStyle={{ maxWidth: 500, visibility: displayTooltip ? 'visible' : 'hidden' }}
overlayStyle={{ maxWidth: 750, visibility: displayTooltip ? 'visible' : 'hidden' }}
style={{ width: '100%' }}
title={<AutoCompleteTooltipContent entity={entity} />}
placement="top"

View File

@ -53,7 +53,7 @@ export default function AutoCompleteTooltipContent({ entity }: Props) {
}
queryCountLast30Days={(entity as Dataset).statsSummary?.queryCountLast30Days}
uniqueUserCountLast30Days={(entity as Dataset).statsSummary?.uniqueUserCountLast30Days}
color="" // need to pass in empty color so that tooltip decides the color here
mode="tooltip-content"
/>
)}
</ContentWrapper>

View File

@ -1,20 +1,31 @@
const intlFormat = (num) => {
return new Intl.NumberFormat().format(Math.round(num * 10) / 10);
};
type NumMapType = Record<'billion' | 'million' | 'thousand', { value: number; symbol: string }>;
export const countFormatter: (num: number) => string = (num: number) => {
if (num >= 1000000000) {
return `${intlFormat(num / 1000000000)}B`;
}
if (num >= 1000000) {
return `${intlFormat(num / 1000000)}M`;
}
const NumMap: NumMapType = {
billion: {
value: 1000000000,
symbol: 'B',
},
million: {
value: 1000000,
symbol: 'M',
},
thousand: {
value: 1000,
symbol: 'K',
},
} as const;
if (num >= 1000) return `${intlFormat(num / 1000)}K`;
const isBillions = (num: number) => num >= NumMap.billion.value;
const isMillions = (num: number) => num >= NumMap.million.value;
const isThousands = (num: number) => num >= NumMap.thousand.value;
const intlFormat = (num: number) => new Intl.NumberFormat().format(Math.round(num * 10) / 10);
export const needsFormatting = (num: number) => isThousands(num);
export const countFormatter = (num: number) => {
if (isBillions(num)) return `${intlFormat(num / NumMap.billion.value)}${NumMap.billion.symbol}`;
if (isMillions(num)) return `${intlFormat(num / NumMap.million.value)}${NumMap.million.symbol}`;
if (isThousands(num)) return `${intlFormat(num / NumMap.thousand.value)}${NumMap.thousand.symbol}`;
return intlFormat(num);
};
export const countSeparator = (num) => {
return num.toLocaleString();
};