mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Responsive fixes.
This commit is contained in:
parent
3d8402d2f1
commit
8a66603d32
16 changed files with 302 additions and 231 deletions
|
|
@ -95,7 +95,7 @@ export function Attribution({
|
|||
})}
|
||||
</MetricsBar>
|
||||
<SectionHeader title={formatMessage(labels.sources)} />
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||
<Panel>
|
||||
<AttributionTable data={data?.['referrer']} title={formatMessage(labels.referrer)} />
|
||||
</Panel>
|
||||
|
|
@ -104,7 +104,7 @@ export function Attribution({
|
|||
</Panel>
|
||||
</Grid>
|
||||
<SectionHeader title="UTM" />
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||
<Panel>
|
||||
<AttributionTable data={data?.['utm_source']} title={formatMessage(labels.sources)} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap="6">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Grid columns="1fr 1fr 1fr" gap>
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr 1fr' }} gap>
|
||||
<Column>
|
||||
<Select
|
||||
label={formatMessage(labels.model)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Text, DataTable, DataColumn } from '@umami/react-zen';
|
||||
import { Text, DataTable, DataColumn, Column } from '@umami/react-zen';
|
||||
import { useMessages, useResultQuery, useFormat, useFields } from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { formatShortTime } from '@/lib/format';
|
||||
|
|
@ -27,43 +27,65 @@ export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }
|
|||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<DataTable data={data}>
|
||||
{selectedFields.map(field => {
|
||||
return (
|
||||
<DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}>
|
||||
{row => {
|
||||
const value = formatValue(row[field], field);
|
||||
return (
|
||||
<Text truncate title={value}>
|
||||
{value}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
);
|
||||
})}
|
||||
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
|
||||
{row => row?.['visitors']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
|
||||
{row => row?.['visits']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="views" label={formatMessage(labels.views)} align="end">
|
||||
{row => row?.['views']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
|
||||
{row => {
|
||||
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
|
||||
return Math.round(+n) + '%';
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="visitDuration" label={formatMessage(labels.visitDuration)} align="end">
|
||||
{row => {
|
||||
const n = row?.['totaltime'] / row?.['visits'];
|
||||
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
<Column overflow="auto" minHeight="0" height="100%">
|
||||
<DataTable data={data} style={{ tableLayout: 'fixed' }}>
|
||||
{selectedFields.map(field => {
|
||||
return (
|
||||
<DataColumn
|
||||
key={field}
|
||||
id={field}
|
||||
label={fields.find(f => f.name === field)?.label}
|
||||
width="minmax(120px, 1fr)"
|
||||
>
|
||||
{row => {
|
||||
const value = formatValue(row[field], field);
|
||||
return (
|
||||
<Text truncate title={value}>
|
||||
{value}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
);
|
||||
})}
|
||||
<DataColumn
|
||||
id="visitors"
|
||||
label={formatMessage(labels.visitors)}
|
||||
align="end"
|
||||
width="120px"
|
||||
>
|
||||
{row => row?.['visitors']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end" width="120px">
|
||||
{row => row?.['visits']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="views" label={formatMessage(labels.views)} align="end" width="120px">
|
||||
{row => row?.['views']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn
|
||||
id="bounceRate"
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
align="end"
|
||||
width="120px"
|
||||
>
|
||||
{row => {
|
||||
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
|
||||
return Math.round(+n) + '%';
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn
|
||||
id="visitDuration"
|
||||
label={formatMessage(labels.visitDuration)}
|
||||
align="end"
|
||||
width="120px"
|
||||
>
|
||||
{row => {
|
||||
const n = row?.['totaltime'] / row?.['visits'];
|
||||
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
</Column>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useDateRange, useMessages, useMobile } from '@/components/hooks';
|
||||
import { useDateRange, useMessages } from '@/components/hooks';
|
||||
import { ListCheck } from '@/components/icons';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { Column, Row } from '@umami/react-zen';
|
||||
|
|
@ -14,12 +14,10 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
|||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const [fields, setFields] = useState(['path']);
|
||||
const { isMobile } = useMobile();
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Row alignItems="center" justifyContent={isMobile ? 'flex-end' : 'flex-start'}>
|
||||
<Row alignItems="center" justifyContent="flex-start">
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
</Row>
|
||||
<Panel height="900px" overflow="auto" allowFullscreen>
|
||||
|
|
@ -41,8 +39,9 @@ const FieldsButton = ({ value, onChange }) => {
|
|||
<DialogButton
|
||||
icon={<ListCheck />}
|
||||
label={formatMessage(labels.fields)}
|
||||
width="800px"
|
||||
width="400px"
|
||||
minHeight="300px"
|
||||
variant="outline"
|
||||
>
|
||||
{({ close }) => {
|
||||
return <FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />;
|
||||
|
|
|
|||
|
|
@ -51,51 +51,72 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
|||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Panel allowFullscreen height="900px">
|
||||
<Column gap="1" width="100%" overflow="auto">
|
||||
<Grid
|
||||
columns="120px repeat(10, 100px)"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
height="50px"
|
||||
autoFlow="column"
|
||||
>
|
||||
<Column>
|
||||
<Text weight="bold" align="center">
|
||||
{formatMessage(labels.cohort)}
|
||||
</Text>
|
||||
</Column>
|
||||
{days.map(n => (
|
||||
<Column key={n}>
|
||||
<Text weight="bold" align="center" wrap="nowrap">
|
||||
{formatMessage(labels.day)} {n}
|
||||
<Column
|
||||
paddingY="6"
|
||||
paddingX={{ xs: '3', md: '6' }}
|
||||
position="absolute"
|
||||
top="40px"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
>
|
||||
<Column gap="1" overflow="auto">
|
||||
<Grid
|
||||
columns="120px repeat(10, 100px)"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
height="50px"
|
||||
width="max-content"
|
||||
minWidth="100%"
|
||||
autoFlow="column"
|
||||
>
|
||||
<Column>
|
||||
<Text weight="bold" align="center">
|
||||
{formatMessage(labels.cohort)}
|
||||
</Text>
|
||||
</Column>
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
return (
|
||||
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
|
||||
<Column justifyContent="center" gap="1">
|
||||
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Users />
|
||||
</Icon>
|
||||
<Text>{formatLongNumber(visitors)}</Text>
|
||||
</Row>
|
||||
{days.map(n => (
|
||||
<Column key={n}>
|
||||
<Text weight="bold" align="center" wrap="nowrap">
|
||||
{formatMessage(labels.day)} {n}
|
||||
</Text>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return (
|
||||
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
return (
|
||||
<Grid
|
||||
key={rowIndex}
|
||||
columns="120px repeat(10, 100px)"
|
||||
gap="1"
|
||||
autoFlow="column"
|
||||
width="max-content"
|
||||
minWidth="100%"
|
||||
>
|
||||
<Column justifyContent="center" gap="1">
|
||||
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Users />
|
||||
</Icon>
|
||||
<Text>{formatLongNumber(visitors)}</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return (
|
||||
<Cell key={day}>
|
||||
{percentage ? `${Number(percentage).toFixed(2)}%` : ''}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
</Column>
|
||||
</Panel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
|
|||
|
||||
return (
|
||||
<Panel key={param}>
|
||||
<Grid columns="1fr 1fr">
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap="6">
|
||||
<Column>
|
||||
<Heading>
|
||||
<Text transform="capitalize">{param.replace(/^utm_/, '')}</Text>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export function ExpandedViewModal({
|
|||
maxWidth: 1320,
|
||||
width: '100vw',
|
||||
height: isMobile ? '100dvh' : 'calc(100dvh - 40px)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{({ close }) => {
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ export function WebsiteControls({
|
|||
return (
|
||||
<Column gap>
|
||||
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap>
|
||||
<Row alignItems="center" justifyContent="flex-end">
|
||||
<Row alignItems="center" justifyContent="flex-start">
|
||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||
</Row>
|
||||
<Row alignItems="center" justifyContent="flex-end">
|
||||
<Row alignItems="center" justifyContent={{ xs: 'flex-start', md: 'flex-end' }}>
|
||||
{allowDateFilter && (
|
||||
<WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,17 +19,23 @@ export function WebsiteExpandedView({
|
|||
} = useNavigation();
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Row display={{ xs: 'flex', md: 'none' }}>
|
||||
<Column height="100%" overflow="hidden" gap>
|
||||
<Row id="expanded-mobile-menu-button" display={{ xs: 'flex', md: 'none' }}>
|
||||
<MobileMenuButton>
|
||||
{({ close }) => {
|
||||
return <WebsiteExpandedMenu excludedIds={excludedIds} onItemClick={close} />;
|
||||
return (
|
||||
<Column padding="3">
|
||||
<WebsiteExpandedMenu excludedIds={excludedIds} onItemClick={close} />
|
||||
</Column>
|
||||
);
|
||||
}}
|
||||
</MobileMenuButton>
|
||||
</Row>
|
||||
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" height="100%" overflow="hidden">
|
||||
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" overflow="hidden">
|
||||
<Column
|
||||
id="metrics-expanded-menu"
|
||||
display={{ xs: 'none', md: 'flex' }}
|
||||
width="240px"
|
||||
gap="6"
|
||||
border="right"
|
||||
paddingRight="3"
|
||||
|
|
@ -37,7 +43,7 @@ export function WebsiteExpandedView({
|
|||
>
|
||||
<WebsiteExpandedMenu excludedIds={excludedIds} />
|
||||
</Column>
|
||||
<Column overflow="hidden">
|
||||
<Column id="metrics-expanded-table" overflow="hidden">
|
||||
<MetricsExpandedTable
|
||||
title={formatMessage(labels[view])}
|
||||
type={view}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,11 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
|||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} marginBottom="3">
|
||||
<Row alignItems="center" gap="6">
|
||||
<Row alignItems="center" gap="6" wrap="wrap">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
|
||||
{showActions && (
|
||||
<Row
|
||||
display={{ xs: 'none', sm: 'none', md: 'none', lg: 'flex', xl: 'flex' }}
|
||||
alignItems="center"
|
||||
gap
|
||||
>
|
||||
<Row alignItems="center" gap>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
<Icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
|
||||
import { Heading, Icon, Row, Text, Column, Grid } from '@umami/react-zen';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
|
|
@ -8,7 +8,6 @@ export function PageHeader({
|
|||
icon,
|
||||
showBorder = true,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
@ -18,16 +17,13 @@ export function PageHeader({
|
|||
allowEdit?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
} & RowProps) {
|
||||
}) {
|
||||
return (
|
||||
<Row
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
<Grid
|
||||
columns={{ xs: '1fr', md: '1fr 1fr' }}
|
||||
paddingY="6"
|
||||
marginBottom="6"
|
||||
border={showBorder ? 'bottom' : undefined}
|
||||
width="100%"
|
||||
{...props}
|
||||
>
|
||||
<Column gap="2">
|
||||
{label}
|
||||
|
|
@ -46,6 +42,6 @@ export function PageHeader({
|
|||
)}
|
||||
</Column>
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export function WebsiteDateFilter({
|
|||
query: { compare = 'prev', offset = 0 },
|
||||
} = useNavigation();
|
||||
const disableForward = isAllTime || isAfter(dateRange.endDate, new Date());
|
||||
const showCompare = allowCompare && !isAllTime;
|
||||
|
||||
const websiteDateRange = useDateRangeQuery(websiteId);
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ export function WebsiteDateFilter({
|
|||
}, [dateRange]);
|
||||
|
||||
return (
|
||||
<Row gap>
|
||||
<Row wrap="wrap" gap>
|
||||
{showButtons && !isAllTime && !isCustomRange && (
|
||||
<Row gap="1">
|
||||
<Button onPress={() => handleIncrement(-1)} variant="outline">
|
||||
|
|
@ -85,7 +86,7 @@ export function WebsiteDateFilter({
|
|||
renderDate={+offset !== 0}
|
||||
/>
|
||||
</Row>
|
||||
{allowCompare && !isAllTime && (
|
||||
{showCompare && (
|
||||
<Row alignItems="center" gap>
|
||||
<Text weight="bold">VS</Text>
|
||||
<Row width="200px">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface MetricsBarProps extends GridProps {
|
|||
|
||||
export function MetricsBar({ children, ...props }: MetricsBarProps) {
|
||||
return (
|
||||
<Grid columns="repeat(auto-fit, minmax(140px, 1fr))" gap {...props}>
|
||||
<Grid columns="repeat(auto-fit, minmax(160px, 1fr))" gap {...props}>
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const IP_ADDRESS_HEADERS = [
|
|||
'fastly-client-ip', // Fastly
|
||||
'x-nf-client-connection-ip', // Netlify
|
||||
'do-connecting-ip', // Digital Ocean
|
||||
'x-appengine-user-ip', // Google App Ending
|
||||
'x-appengine-user-ip', // Google App Engine
|
||||
'x-client-ip',
|
||||
'x-cluster-client-ip',
|
||||
'x-forwarded',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue