Fixed attribution report. New metric cards. Converted ListTable.

This commit is contained in:
Mike Cao 2025-06-11 00:05:34 -07:00
parent b2aa37a3df
commit 4995a0e1e4
19 changed files with 167 additions and 288 deletions

View file

@ -2,6 +2,7 @@
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.row.inactive {

View file

@ -15,6 +15,7 @@ import { useMessages } from '@/components/hooks';
export interface PanelProps extends ColumnProps {
title?: string;
allowFullscreen?: boolean;
noPadding?: boolean;
}
const fullscreenStyles = {
@ -27,7 +28,14 @@ const fullscreenStyles = {
zIndex: 9999,
} as any;
export function Panel({ title, allowFullscreen, style, children, ...props }: PanelProps) {
export function Panel({
title,
allowFullscreen,
noPadding,
style,
children,
...props
}: PanelProps) {
const { formatMessage, labels } = useMessages();
const [isFullscreen, setIsFullscreen] = useState(false);
@ -37,7 +45,7 @@ export function Panel({ title, allowFullscreen, style, children, ...props }: Pan
return (
<Column
padding="6"
padding={!noPadding ? '6' : undefined}
border
borderRadius="3"
backgroundColor

View file

@ -1,110 +0,0 @@
.table {
position: relative;
display: grid;
grid-template-rows: fit-content(100%) auto;
overflow: hidden;
flex: 1;
}
.body {
position: relative;
height: 100%;
overflow: auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
line-height: 40px;
}
.title {
display: flex;
font-weight: 600;
}
.metric {
font-weight: 600;
text-align: center;
width: 100px;
}
.row {
position: relative;
height: 30px;
line-height: 30px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
overflow: hidden;
border-radius: 4px;
}
.row:hover {
background-color: var(--base-color-2);
}
.label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex: 2;
padding-left: 10px;
}
.label a {
color: inherit;
text-decoration: none;
}
.label a:hover {
color: var(--primary-color);
}
.label:empty {
color: #b3b3b3;
}
.label:empty:before {
content: 'Unknown';
}
.value {
display: flex;
align-items: center;
gap: 10px;
text-align: end;
margin-inline-end: 5px;
font-weight: 600;
}
.percent {
position: relative;
width: 50px;
color: var(--base-color-9);
border-inline-start: 1px solid var(--base-color-9);
padding-inline-start: 10px;
z-index: 1;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 30px;
opacity: 0.1;
background: var(--primary-color);
z-index: -1;
}
.empty {
min-height: 200px;
}
@media only screen and (max-width: 992px) {
.body {
height: auto;
}
}

View file

@ -1,12 +1,11 @@
import { ReactNode } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, config } from '@react-spring/web';
import classNames from 'classnames';
import { Grid, Row, Column, Text } from '@umami/react-zen';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Empty } from '@/components/common/Empty';
import { useMessages } from '@/components/hooks';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import styles from './ListTable.module.css';
const ITEM_SIZE = 30;
@ -28,7 +27,6 @@ export function ListTable({
data = [],
title,
metric,
className,
renderLabel,
renderChange,
animate = true,
@ -40,7 +38,7 @@ export function ListTable({
const { formatMessage, labels } = useMessages();
const getRow = (row: { x: any; y: any; z: any }, index: number) => {
const { x: label, y: value, z: percent } = row;
const { x: label, y: value, z: percent } = row || {};
return (
<AnimatedRow
@ -56,18 +54,20 @@ export function ListTable({
);
};
const Row = ({ index, style }) => {
const ListTableRow = ({ index, style }) => {
return <div style={style}>{getRow(data[index], index)}</div>;
};
return (
<div className={classNames(styles.table, className)}>
<div className={styles.header}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>{metric}</div>
</div>
<div className={styles.body}>
{data?.length === 0 && <Empty className={styles.empty} />}
<Column gap>
<Grid alignItems="center" justifyContent="space-between" paddingLeft="2" columns="1fr 100px">
<Text weight="bold">{title}</Text>
<Text weight="bold" align="center">
{metric}
</Text>
</Grid>
<Column gap="1">
{data?.length === 0 && <Empty />}
{virtualize && data.length > 0 ? (
<FixedSizeList
width="100%"
@ -75,13 +75,13 @@ export function ListTable({
itemCount={data.length}
itemSize={ITEM_SIZE}
>
{Row}
{ListTableRow}
</FixedSizeList>
) : (
data.map(getRow)
)}
</div>
</div>
</Column>
</Column>
);
}
@ -102,22 +102,33 @@ const AnimatedRow = ({
});
return (
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<div className={styles.value}>
<Grid columns="1fr 50px 50px" paddingLeft="2" alignItems="center" hoverBackgroundColor="2" gap>
<Row alignItems="center">
<Text>{label}</Text>
</Row>
<Row alignItems="center" height="30px" justifyContent="flex-end">
{change}
<AnimatedDiv className={styles.value} title={props?.y as any}>
{currency
? props.y?.to(n => formatLongCurrency(n, currency))
: props.y?.to(formatLongNumber)}
</AnimatedDiv>
</div>
<Text weight="bold">
<AnimatedDiv title={props?.y as any}>
{currency
? props.y?.to(n => formatLongCurrency(n, currency))
: props.y?.to(formatLongNumber)}
</AnimatedDiv>
</Text>
</Row>
{showPercentage && (
<div className={styles.percent}>
<AnimatedDiv className={styles.bar} style={{ width: props.width.to(n => `${n}%`) }} />
<Row
alignItems="center"
justifyContent="flex-start"
position="relative"
border="left"
borderColor="8"
color="muted"
paddingLeft="3"
>
<AnimatedDiv>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</AnimatedDiv>
</div>
</Row>
)}
</div>
</Grid>
);
};

View file

@ -1,7 +0,0 @@
.card {
border-right: 1px solid var(--border-color);
}
.card:last-child {
border-right: 0;
}

View file

@ -3,7 +3,6 @@ import { useSpring } from '@react-spring/web';
import { formatNumber } from '@/lib/format';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import styles from './MetricCard.module.css';
export interface MetricCardProps {
value: number;
@ -34,7 +33,14 @@ export const MetricCard = ({
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return (
<Column className={styles.card} justifyContent="center" paddingX="8">
<Column
justifyContent="center"
paddingX="6"
paddingY="4"
borderRadius="3"
backgroundColor
border
>
{showLabel && (
<Text weight="bold" wrap="nowrap">
{label}

View file

@ -1,24 +1,22 @@
import { ReactNode } from 'react';
import { Grid, Loading } from '@umami/react-zen';
import { ErrorMessage } from '@/components/common/ErrorMessage';
import { Grid } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
export interface MetricsBarProps {
isLoading?: boolean;
isFetched?: boolean;
error?: unknown;
error?: Error;
children?: ReactNode;
}
export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) {
return (
<>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
<LoadingPanel isLoading={isLoading} isFetched={isFetched} error={error}>
{!isLoading && !error && isFetched && (
<Grid columns="repeat(auto-fill, minmax(200px, 1fr))" width="100%" gapY="3">
<Grid columns="repeat(auto-fit, minmax(200px, 1fr))" width="100%" gap>
{children}
</Grid>
)}
</>
</LoadingPanel>
);
}