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

View File

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

View File

@ -7,6 +7,8 @@ import { ANTD_GRAY } from '../../shared/constants';
import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils'; import { toLocalDateTimeString, toRelativeTimeString } from '../../../shared/time/timeUtils';
import { StatsSummary } from '../../shared/components/styled/StatsSummary'; import { StatsSummary } from '../../shared/components/styled/StatsSummary';
import { FormattedBytesStat } from './FormattedBytesStat'; import { FormattedBytesStat } from './FormattedBytesStat';
import { countFormatter, needsFormatting } from '../../../../utils/formatter';
import ExpandingStat from './ExpandingStat';
const StatText = styled.span<{ color: string }>` const StatText = styled.span<{ color: string }>`
color: ${(props) => props.color}; color: ${(props) => props.color};
@ -25,6 +27,7 @@ type Props = {
uniqueUserCountLast30Days?: number | null; uniqueUserCountLast30Days?: number | null;
lastUpdatedMs?: number | null; lastUpdatedMs?: number | null;
color?: string; color?: string;
mode?: 'normal' | 'tooltip-content';
}; };
export const DatasetStatsSummary = ({ export const DatasetStatsSummary = ({
@ -36,20 +39,33 @@ export const DatasetStatsSummary = ({
uniqueUserCountLast30Days, uniqueUserCountLast30Days,
lastUpdatedMs, lastUpdatedMs,
color, color,
mode = 'normal',
}: Props) => { }: Props) => {
const displayedColor = color !== undefined ? color : ANTD_GRAY[7]; const isTooltipMode = mode === 'tooltip-content';
const displayedColor = isTooltipMode ? '' : color ?? ANTD_GRAY[7];
const statsViews = [ const statsViews = [
!!rowCount && ( !!rowCount && (
<ExpandingStat
disabled={isTooltipMode || !needsFormatting(rowCount)}
render={(isExpanded) => (
<StatText color={displayedColor}> <StatText color={displayedColor}>
<TableOutlined style={{ marginRight: 8, color: displayedColor }} /> <TableOutlined style={{ marginRight: 8, color: displayedColor }} />
<b>{formatNumberWithoutAbbreviation(rowCount)}</b> rows <b>{isExpanded ? formatNumberWithoutAbbreviation(rowCount) : countFormatter(rowCount)}</b> rows
{!!columnCount && ( {!!columnCount && (
<> <>
, <b>{formatNumberWithoutAbbreviation(columnCount)}</b> columns ,{' '}
<b>
{isExpanded
? formatNumberWithoutAbbreviation(columnCount)
: countFormatter(columnCount)}
</b>{' '}
columns
</> </>
)} )}
</StatText> </StatText>
)}
/>
), ),
!!sizeInBytes && ( !!sizeInBytes && (
<StatText color={displayedColor}> <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` const StatsContainer = styled.div`
margin-top: 8px; margin-top: 8px;
display: flex;
align-items: center;
`; `;
const StatDivider = styled.div` const StatDivider = styled.div`
display: inline-block;
padding-left: 10px; padding-left: 10px;
margin-right: 10px; margin-right: 10px;
border-right: 1px solid ${ANTD_GRAY[4]}; border-right: 1px solid ${ANTD_GRAY[4]};
height: 21px; height: 21px;
vertical-align: text-top;
`; `;
export const StatsSummary = ({ stats }: Props) => { export const StatsSummary = ({ stats }: Props) => {
@ -25,10 +25,10 @@ export const StatsSummary = ({ stats }: Props) => {
{stats && stats.length > 0 && ( {stats && stats.length > 0 && (
<StatsContainer> <StatsContainer>
{stats.map((statView, index) => ( {stats.map((statView, index) => (
<span> <>
{statView} {statView}
{index < stats.length - 1 && <StatDivider />} {index < stats.length - 1 && <StatDivider />}
</span> </>
))} ))}
</StatsContainer> </StatsContainer>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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