mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
581ddc0233
7 changed files with 45 additions and 103 deletions
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
|
@ -367,8 +367,6 @@ importers:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
dist: {}
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,33 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { Grid, Row, Text } from '@umami/react-zen';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { colord } from 'colord';
|
|
||||||
import { BarChart } from '@/components/charts/BarChart';
|
import { BarChart } from '@/components/charts/BarChart';
|
||||||
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||||
import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
||||||
|
import { CurrencySelect } from '@/components/input/CurrencySelect';
|
||||||
import { ListTable } from '@/components/metrics/ListTable';
|
import { ListTable } from '@/components/metrics/ListTable';
|
||||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
import { CHART_COLORS } from '@/lib/constants';
|
import { CHART_COLORS } from '@/lib/constants';
|
||||||
|
import { generateTimeSeries } from '@/lib/date';
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import classNames from 'classnames';
|
||||||
import { Column } from '@umami/react-zen';
|
import { colord } from 'colord';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { getMinimumUnit } from '@/lib/date';
|
|
||||||
import { CurrencySelect } from '@/components/input/CurrencySelect';
|
|
||||||
|
|
||||||
export interface RevenueProps {
|
export interface RevenueProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState('USD');
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { locale } = useLocale();
|
const { locale, dateLocale } = useLocale();
|
||||||
const { countryNames } = useCountryNames(locale);
|
const { countryNames } = useCountryNames(locale);
|
||||||
const unit = getMinimumUnit(startDate, endDate);
|
|
||||||
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
|
|
@ -65,7 +63,7 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
||||||
return {
|
return {
|
||||||
label: key,
|
label: key,
|
||||||
data: map[key],
|
data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
|
||||||
lineTension: 0,
|
lineTension: 0,
|
||||||
backgroundColor: color.alpha(0.6).toRgbString(),
|
backgroundColor: color.alpha(0.6).toRgbString(),
|
||||||
borderColor: color.alpha(0.7).toRgbString(),
|
borderColor: color.alpha(0.7).toRgbString(),
|
||||||
|
|
@ -104,6 +102,8 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
] as any;
|
] as any;
|
||||||
}, [data, locale]);
|
}, [data, locale]);
|
||||||
|
|
||||||
|
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<Grid columns="280px" gap>
|
<Grid columns="280px" gap>
|
||||||
|
|
@ -127,7 +127,7 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
unit={unit}
|
unit={unit}
|
||||||
stacked={true}
|
stacked={true}
|
||||||
currency={currency}
|
currency={currency}
|
||||||
renderXLabel={renderDateLabels(unit, locale)}
|
renderXLabel={renderXLabel}
|
||||||
height="400px"
|
height="400px"
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import { useDateRange } from '@/components/hooks';
|
||||||
|
|
||||||
export function RevenuePage({ websiteId }: { websiteId: string }) {
|
export function RevenuePage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate, unit },
|
||||||
} = useDateRange(websiteId);
|
} = useDateRange(websiteId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<Revenue websiteId={websiteId} startDate={startDate} endDate={endDate} />
|
<Revenue websiteId={websiteId} startDate={startDate} endDate={endDate} unit={unit} />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ export function WebsiteTransferForm({
|
||||||
const isTeamWebsite = !!website?.teamId;
|
const isTeamWebsite = !!website?.teamId;
|
||||||
|
|
||||||
const items =
|
const items =
|
||||||
teams?.data?.filter(({ teamUser }) =>
|
teams?.data?.filter(({ members }) =>
|
||||||
teamUser.find(
|
members.some(
|
||||||
({ role, userId }) =>
|
({ role, userId }) =>
|
||||||
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
||||||
),
|
),
|
||||||
|
|
@ -79,7 +79,7 @@ export function WebsiteTransferForm({
|
||||||
<Select onSelectionChange={handleChange} selectedKey={teamId}>
|
<Select onSelectionChange={handleChange} selectedKey={teamId}>
|
||||||
{items.map(({ id, name }) => {
|
{items.map(({ id, name }) => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={`${id}!!!!`} id={`${id}????`}>
|
<ListItem key={`${id}`} id={`${id}`}>
|
||||||
{name}
|
{name}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,11 +73,13 @@ export function MetricsTable({
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
minHeight="380px"
|
minHeight="400px"
|
||||||
>
|
>
|
||||||
{data && <ListTable {...props} data={filteredData} renderLabel={renderLabel} />}
|
<div style={{ display: 'grid', gridTemplateRows: '1fr auto', minHeight: '400px' }}>
|
||||||
|
<div>{data && <ListTable {...props} data={filteredData} renderLabel={renderLabel} />}</div>
|
||||||
|
<div>
|
||||||
{showMore && limit && (
|
{showMore && limit && (
|
||||||
<Row justifyContent="center">
|
<Row justifyContent="center" alignItems="flex-end">
|
||||||
<LinkButton href={updateParams({ view: type })} variant="quiet">
|
<LinkButton href={updateParams({ view: type })} variant="quiet">
|
||||||
<Icon size="sm">
|
<Icon size="sm">
|
||||||
<Maximize />
|
<Maximize />
|
||||||
|
|
@ -86,6 +88,8 @@ export function MetricsTable({
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</LoadingPanel>
|
</LoadingPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,9 @@ export function parseDateValue(value: string) {
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
||||||
const { num, unit } = match.groups;
|
const { num, unit } = match.groups;
|
||||||
|
const formattedNum = +num > 0 ? +num - 1 : +num;
|
||||||
|
|
||||||
return { num: +num, unit };
|
return { num: formattedNum, unit };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDateRange(value: string, locale = 'en-US'): DateRange {
|
export function parseDateRange(value: string, locale = 'en-US'): DateRange {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface RevenuParameters {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
timezone: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,12 +15,6 @@ export interface RevenueResult {
|
||||||
chart: { x: string; t: string; y: number }[];
|
chart: { x: string; t: string; y: number }[];
|
||||||
country: { name: string; value: number }[];
|
country: { name: string; value: number }[];
|
||||||
total: { sum: number; count: number; average: number; unique_count: number };
|
total: { sum: number; count: number; average: number; unique_count: number };
|
||||||
table: {
|
|
||||||
currency: string;
|
|
||||||
sum: number;
|
|
||||||
count: number;
|
|
||||||
unique_count: number;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRevenue(
|
export async function getRevenue(
|
||||||
|
|
@ -36,7 +31,7 @@ async function relationalQuery(
|
||||||
parameters: RevenuParameters,
|
parameters: RevenuParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<RevenueResult> {
|
): Promise<RevenueResult> {
|
||||||
const { startDate, endDate, currency, unit = 'day' } = parameters;
|
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
||||||
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
||||||
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||||
...filters,
|
...filters,
|
||||||
|
|
@ -50,7 +45,7 @@ async function relationalQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
revenue.event_name x,
|
revenue.event_name x,
|
||||||
${getDateSQL('revenue.created_at', unit)} t,
|
${getDateSQL('revenue.created_at', unit, timezone)} t,
|
||||||
sum(revenue.revenue) y
|
sum(revenue.revenue) y
|
||||||
from revenue
|
from revenue
|
||||||
join website_event
|
join website_event
|
||||||
|
|
@ -121,32 +116,7 @@ async function relationalQuery(
|
||||||
|
|
||||||
total.average = total.count > 0 ? total.sum / total.count : 0;
|
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||||
|
|
||||||
const table = await rawQuery(
|
return { chart, country, total };
|
||||||
`
|
|
||||||
select
|
|
||||||
revenue.currency,
|
|
||||||
sum(revenue.revenue) as sum,
|
|
||||||
count(distinct revenue.event_id) as count,
|
|
||||||
count(distinct revenue.session_id) as unique_count
|
|
||||||
from revenue
|
|
||||||
join website_event
|
|
||||||
on website_event.website_id = revenue.website_id
|
|
||||||
and website_event.session_id = revenue.session_id
|
|
||||||
and website_event.event_id = revenue.event_id
|
|
||||||
and website_event.website_id = {{websiteId::uuid}}
|
|
||||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
|
||||||
${cohortQuery}
|
|
||||||
${joinSessionQuery}
|
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
|
||||||
${filterQuery}
|
|
||||||
group by revenue.currency
|
|
||||||
order by sum desc
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { chart, country, table, total };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickhouseQuery(
|
async function clickhouseQuery(
|
||||||
|
|
@ -154,7 +124,7 @@ async function clickhouseQuery(
|
||||||
parameters: RevenuParameters,
|
parameters: RevenuParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<RevenueResult> {
|
): Promise<RevenueResult> {
|
||||||
const { startDate, endDate, unit = 'day', currency } = parameters;
|
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
||||||
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
||||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
...filters,
|
...filters,
|
||||||
|
|
@ -174,7 +144,7 @@ async function clickhouseQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
website_revenue.event_name x,
|
website_revenue.event_name x,
|
||||||
${getDateSQL('website_revenue.created_at', unit)} t,
|
${getDateSQL('website_revenue.created_at', unit, timezone)} t,
|
||||||
sum(website_revenue.revenue) y
|
sum(website_revenue.revenue) y
|
||||||
from website_revenue
|
from website_revenue
|
||||||
join website_event
|
join website_event
|
||||||
|
|
@ -250,36 +220,5 @@ async function clickhouseQuery(
|
||||||
|
|
||||||
total.average = total.count > 0 ? total.sum / total.count : 0;
|
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||||
|
|
||||||
const table = await rawQuery<
|
return { chart, country, total };
|
||||||
{
|
|
||||||
currency: string;
|
|
||||||
sum: number;
|
|
||||||
count: number;
|
|
||||||
unique_count: number;
|
|
||||||
}[]
|
|
||||||
>(
|
|
||||||
`
|
|
||||||
select
|
|
||||||
website_revenue.currency,
|
|
||||||
sum(website_revenue.revenue) as sum,
|
|
||||||
uniqExact(website_revenue.event_id) as count,
|
|
||||||
uniqExact(website_revenue.session_id) as unique_count
|
|
||||||
from website_revenue
|
|
||||||
join website_event
|
|
||||||
on website_event.website_id = website_revenue.website_id
|
|
||||||
and website_event.session_id = website_revenue.session_id
|
|
||||||
and website_event.event_id = website_revenue.event_id
|
|
||||||
and website_event.website_id = {websiteId:UUID}
|
|
||||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
${cohortQuery}
|
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
${filterQuery}
|
|
||||||
group by website_revenue.currency
|
|
||||||
order by sum desc
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { chart, country, table, total };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue