mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-03 04:10:43 +00:00
feat(stats): make rowcount more human readable (#8232)
This commit is contained in:
parent
c5cc53b99a
commit
071ef4d111
@ -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 && (
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user