mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Fixed attribution report. New metric cards. Converted ListTable.
This commit is contained in:
parent
b2aa37a3df
commit
4995a0e1e4
19 changed files with 167 additions and 288 deletions
|
|
@ -17,9 +17,7 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
||||
<Panel>
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
|
||||
</Panel>
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
|
||||
<Panel>
|
||||
<WebsiteChart websiteId={websiteId} compareMode={compare} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
|||
</Panel>
|
||||
</GridRow>
|
||||
<GridRow layout="two-one">
|
||||
<Panel padding="0" gridColumn="span 2">
|
||||
<Panel gridColumn="span 2" noPadding>
|
||||
<WorldMap websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ export function EventsPage({ websiteId }) {
|
|||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Panel>
|
||||
<EventsMetricsBar websiteId={websiteId} />
|
||||
</Panel>
|
||||
<EventsMetricsBar websiteId={websiteId} />
|
||||
<GridRow layout="two-one">
|
||||
<Panel gridColumn="span 2">
|
||||
<EventsChart websiteId={websiteId} focusLabel={label} />
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { RealtimeUrls } from './RealtimeUrls';
|
|||
import { RealtimeCountries } from './RealtimeCountries';
|
||||
import { percentFilter } from '@/lib/filters';
|
||||
|
||||
export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
||||
export function RealtimePage({ websiteId }: { websiteId: string }) {
|
||||
const { data, isLoading, error } = useRealtimeQuery(websiteId);
|
||||
|
||||
if (isLoading || error) {
|
||||
|
|
@ -28,9 +28,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<Grid gap="3">
|
||||
<Panel>
|
||||
<RealtimeHeader data={data} />
|
||||
</Panel>
|
||||
<RealtimeHeader data={data} />
|
||||
<Panel>
|
||||
<RealtimeChart data={data} unit="minute" />
|
||||
</Panel>
|
||||
|
|
@ -46,7 +44,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
|||
<Panel>
|
||||
<RealtimeCountries data={countries} />
|
||||
</Panel>
|
||||
<Panel padding="0" gridColumn="span 2">
|
||||
<Panel gridColumn="span 2" noPadding>
|
||||
<WorldMap data={countries} />
|
||||
</Panel>
|
||||
</GridRow>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { WebsiteRealtimePage } from './WebsiteRealtimePage';
|
||||
import { RealtimePage } from './RealtimePage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <WebsiteRealtimePage websiteId={websiteId} />;
|
||||
return <RealtimePage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { Grid, Column } from '@umami/react-zen';
|
|||
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { CHART_COLORS } from '@/lib/constants';
|
||||
|
||||
import { PieChart } from '@/components/charts/PieChart';
|
||||
import { ListTable } from '@/components/metrics/ListTable';
|
||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { percentFilter } from '@/lib/filters';
|
||||
|
||||
export interface AttributionProps {
|
||||
websiteId: string;
|
||||
|
|
@ -44,16 +43,8 @@ export function Attribution({
|
|||
const isEmpty = !Object.keys(data || {}).length;
|
||||
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const ATTRIBUTION_PARAMS = [
|
||||
{ value: 'referrer', label: formatMessage(labels.referrers) },
|
||||
{ value: 'paidAds', label: formatMessage(labels.paidAds) },
|
||||
];
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { pageviews, visitors, visits } = data.total;
|
||||
const { pageviews, visitors, visits } = data?.total || {};
|
||||
|
||||
const metrics = data
|
||||
? [
|
||||
|
|
@ -75,22 +66,18 @@ export function Attribution({
|
|||
]
|
||||
: [];
|
||||
|
||||
function UTMTable(UTMTableProps: { data: any; title: string; utm: string }) {
|
||||
const { data, title, utm } = UTMTableProps;
|
||||
const total = data[utm].reduce((sum, { value }) => {
|
||||
return +sum + +value;
|
||||
}, 0);
|
||||
|
||||
function UTMTable({ data = [], title }: { data: any; title: string }) {
|
||||
return (
|
||||
<ListTable
|
||||
title={title}
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={data[utm].map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
z: (value / total) * 100,
|
||||
}))}
|
||||
data={percentFilter(
|
||||
data.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -98,58 +85,58 @@ export function Attribution({
|
|||
return (
|
||||
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
|
||||
<Column gap>
|
||||
<Panel>
|
||||
<MetricsBar isFetched={data}>
|
||||
{metrics?.map(({ label, value, formatValue }) => {
|
||||
return (
|
||||
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
</Panel>
|
||||
{ATTRIBUTION_PARAMS.map(({ value, label }) => {
|
||||
const items = data[value];
|
||||
const total = items.reduce((sum, { value }) => {
|
||||
return +sum + +value;
|
||||
}, 0);
|
||||
|
||||
const chartData = {
|
||||
labels: items.map(({ name }) => name),
|
||||
datasets: [
|
||||
{
|
||||
data: items.map(({ value }) => value),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel key={value} title={label}>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<ListTable
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={items.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
z: (value / total) * 100,
|
||||
}))}
|
||||
/>
|
||||
<PieChart type="doughnut" data={chartData} isLoading={isLoading} />
|
||||
</Grid>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
<Panel title="UTM">
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<UTMTable data={data} title={formatMessage(labels.sources)} utm={'utm_source'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.medium)} utm={'utm_medium'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.campaigns)} utm={'utm_campaign'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.content)} utm={'utm_content'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.terms)} utm={'utm_term'} />
|
||||
</Grid>
|
||||
</Panel>
|
||||
<MetricsBar isFetched={data}>
|
||||
{metrics?.map(({ label, value, formatValue }) => {
|
||||
return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
|
||||
})}
|
||||
</MetricsBar>
|
||||
<SectionHeader title={formatMessage(labels.sources)} />
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Panel>
|
||||
<ListTable
|
||||
title={formatMessage(labels.referrer)}
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={percentFilter(
|
||||
data?.['referrer']?.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<ListTable
|
||||
title={formatMessage(labels.paidAds)}
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={percentFilter(
|
||||
data?.['paidAds']?.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</Panel>
|
||||
</Grid>
|
||||
<SectionHeader title="UTM" />
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Panel>
|
||||
<UTMTable data={data?.['utm_source']} title={formatMessage(labels.sources)} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data?.['utm_medium']} title={formatMessage(labels.medium)} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data?.['utm_cmapaign']} title={formatMessage(labels.campaigns)} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data?.['utm_content']} title={formatMessage(labels.content)} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data?.['utm_term']} title={formatMessage(labels.terms)} />
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Column>
|
||||
</LoadingPanel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
|
|||
} = useDateRange(websiteId);
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Column gap="6">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Grid columns="1fr 1fr 1fr" gap>
|
||||
<Column>
|
||||
|
|
@ -46,6 +46,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
|
|||
value={step}
|
||||
defaultValue={step}
|
||||
onSearch={setStep}
|
||||
delay={1000}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
const [fields, setFields] = useState([]);
|
||||
const [fields, setFields] = useState(['url']);
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
|
|
|
|||
|
|
@ -154,15 +154,13 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
|||
|
||||
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
|
||||
<Column gap>
|
||||
<Panel>
|
||||
<MetricsBar isFetched={!!data}>
|
||||
{metricData?.map(({ label, value, formatValue }) => {
|
||||
return (
|
||||
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
</Panel>
|
||||
<MetricsBar isFetched={!!data}>
|
||||
{metricData?.map(({ label, value, formatValue }) => {
|
||||
return (
|
||||
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
{data && (
|
||||
<>
|
||||
<Panel>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,9 @@ export function SessionsPage({ websiteId }) {
|
|||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Panel>
|
||||
<SessionsMetricsBar websiteId={websiteId} />
|
||||
</Panel>
|
||||
<SessionsMetricsBar websiteId={websiteId} />
|
||||
<GridRow layout="two-one">
|
||||
<Panel padding="0" gridColumn="span 2">
|
||||
<Panel gridColumn="span 2" noPadding>
|
||||
<WorldMap websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row.inactive {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
.card {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export const emptyFilter = (data: any[]) => {
|
|||
};
|
||||
|
||||
export const percentFilter = (data: any[]) => {
|
||||
if (!data) return [];
|
||||
const total = data.reduce((n, { y }) => n + y, 0);
|
||||
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ export interface AttributionCriteria {
|
|||
currency?: string;
|
||||
}
|
||||
|
||||
export interface AttributionResult {
|
||||
referrer: { name: string; value: number }[];
|
||||
paidAds: { name: string; value: number }[];
|
||||
utm_source: { name: string; value: number }[];
|
||||
utm_medium: { name: string; value: number }[];
|
||||
utm_campaign: { name: string; value: number }[];
|
||||
utm_content: { name: string; value: number }[];
|
||||
utm_term: { name: string; value: number }[];
|
||||
total: { pageviews: number; visitors: number; visits: number };
|
||||
}
|
||||
|
||||
export async function getAttribution(...args: [websiteId: string, criteria: AttributionCriteria]) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
|
|
@ -22,20 +33,11 @@ export async function getAttribution(...args: [websiteId: string, criteria: Attr
|
|||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
criteria: AttributionCriteria,
|
||||
): Promise<{
|
||||
referrer: { name: string; value: number }[];
|
||||
paidAds: { name: string; value: number }[];
|
||||
utm_source: { name: string; value: number }[];
|
||||
utm_medium: { name: string; value: number }[];
|
||||
utm_campaign: { name: string; value: number }[];
|
||||
utm_content: { name: string; value: number }[];
|
||||
utm_term: { name: string; value: number }[];
|
||||
total: { pageviews: number; visitors: number; visits: number };
|
||||
}> {
|
||||
): Promise<AttributionResult> {
|
||||
const { startDate, endDate, model, type, step, currency } = criteria;
|
||||
const { rawQuery } = prisma;
|
||||
const eventType = type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'url' ? 'url_path' : 'event_name';
|
||||
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'page' ? 'url_path' : 'event_name';
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ async function relationalQuery(
|
|||
group by 1),`;
|
||||
|
||||
function getModelQuery(model: string) {
|
||||
return model === 'firstClick'
|
||||
return model === 'first-click'
|
||||
? `\n
|
||||
model AS (select e.session_id,
|
||||
min(we.created_at) created_at
|
||||
|
|
@ -239,20 +241,11 @@ async function relationalQuery(
|
|||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
criteria: AttributionCriteria,
|
||||
): Promise<{
|
||||
referrer: { name: string; value: number }[];
|
||||
paidAds: { name: string; value: number }[];
|
||||
utm_source: { name: string; value: number }[];
|
||||
utm_medium: { name: string; value: number }[];
|
||||
utm_campaign: { name: string; value: number }[];
|
||||
utm_content: { name: string; value: number }[];
|
||||
utm_term: { name: string; value: number }[];
|
||||
total: { pageviews: number; visitors: number; visits: number };
|
||||
}> {
|
||||
): Promise<AttributionResult> {
|
||||
const { startDate, endDate, model, type, step, currency } = criteria;
|
||||
const { rawQuery } = clickhouse;
|
||||
const eventType = type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'url' ? 'url_path' : 'event_name';
|
||||
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'page' ? 'url_path' : 'event_name';
|
||||
|
||||
function getUTMQuery(utmColumn: string) {
|
||||
return `
|
||||
|
|
@ -296,7 +289,7 @@ async function clickhouseQuery(
|
|||
group by 1),`;
|
||||
|
||||
function getModelQuery(model: string) {
|
||||
return model === 'firstClick'
|
||||
return model === 'first-click'
|
||||
? `\n
|
||||
model AS (select e.session_id,
|
||||
min(we.created_at) created_at
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue