mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-11 17:03:33 +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 { 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 && (
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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`
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user