mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 20:57:17 +01:00
Refactored tables.
This commit is contained in:
parent
600a3d28c3
commit
c8fe93dd9d
56 changed files with 643 additions and 1038 deletions
|
|
@ -1,54 +1,47 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Icon } from '@umami/react-zen';
|
||||
import { Icon, Row } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { ExternalLink } from '@/components/icons';
|
||||
import styles from './FilterLink.module.css';
|
||||
|
||||
export interface FilterLinkProps {
|
||||
id: string;
|
||||
export interface FilterLinkProps extends HTMLAttributes<HTMLDivElement> {
|
||||
type: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
icon?: ReactNode;
|
||||
externalUrl?: string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function FilterLink({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
externalUrl,
|
||||
children,
|
||||
className,
|
||||
}: FilterLinkProps) {
|
||||
export function FilterLink({ type, value, label, externalUrl, icon }: FilterLinkProps) {
|
||||
const [showLink, setShowLink] = useState(false);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { updateParams, query } = useNavigation();
|
||||
const active = query[id] !== undefined;
|
||||
const selected = query[id] === value;
|
||||
const active = query[type] !== undefined;
|
||||
const selected = query[type] === value;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.row, className, {
|
||||
[styles.inactive]: active && !selected,
|
||||
[styles.active]: active && selected,
|
||||
})}
|
||||
<Row
|
||||
alignItems="center"
|
||||
gap
|
||||
fontWeight={active && selected ? 'bold' : undefined}
|
||||
color={active && !selected ? 'muted' : undefined}
|
||||
onMouseOver={() => setShowLink(true)}
|
||||
onMouseOut={() => setShowLink(false)}
|
||||
>
|
||||
{children}
|
||||
{icon}
|
||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||
{value && (
|
||||
<Link href={updateParams({ [id]: `eq.${value}` })} className={styles.label} replace>
|
||||
<Link href={updateParams({ [type]: `eq.${value}` })} replace>
|
||||
{label || value}
|
||||
</Link>
|
||||
)}
|
||||
{externalUrl && (
|
||||
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||
<Icon className={styles.icon}>
|
||||
{externalUrl && showLink && (
|
||||
<a href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||
<Icon color="muted">
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function LoadingPanel({
|
|||
return (
|
||||
<>
|
||||
{/* Show loading spinner only if no data exists */}
|
||||
{(isLoading || isFetching) && !data && (
|
||||
{(isLoading || isFetching) && (
|
||||
<Column position="relative" height="100%" {...props}>
|
||||
<Loading icon={loadingIcon} position="page" />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ export function PageHeader({
|
|||
>
|
||||
<Column>
|
||||
<Row alignItems="center" gap="3">
|
||||
{icon && <Icon size="md">{icon}</Icon>}
|
||||
{icon && (
|
||||
<Icon size="md" color="muted">
|
||||
{icon}
|
||||
</Icon>
|
||||
)}
|
||||
{title && <Heading size="4">{title}</Heading>}
|
||||
</Row>
|
||||
{description && <Text color="muted">{description}</Text>}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export * from './queries/useWebsiteSegmentsQuery';
|
|||
export * from './queries/useWebsiteSessionQuery';
|
||||
export * from './queries/useWebsiteSessionStatsQuery';
|
||||
export * from './queries/useWebsiteSessionsQuery';
|
||||
export * from './queries/useWebsiteSessionsWeeklyQuery';
|
||||
export * from './queries/useWeeklyTrafficQuery';
|
||||
export * from './queries/useWebsiteStatsQuery';
|
||||
export * from './queries/useWebsiteValuesQuery';
|
||||
export * from './queries/useWebsitesQuery';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import { useModified } from '../useModified';
|
|||
import { useDateParameters } from '../useDateParameters';
|
||||
import { useFilterParameters } from '@/components/hooks/useFilterParameters';
|
||||
|
||||
export function useWebsiteSessionsWeeklyQuery(
|
||||
websiteId: string,
|
||||
params?: Record<string, string | number>,
|
||||
) {
|
||||
export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string, string | number>) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { modified } = useModified(`sessions`);
|
||||
const date = useDateParameters(websiteId);
|
||||
|
|
@ -9,7 +9,12 @@ export function useRegionNames(locale: string) {
|
|||
return regions[regionCode];
|
||||
}
|
||||
|
||||
const region = regionCode.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
|
||||
if (!regionCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const region = regionCode?.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
|
||||
|
||||
return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export {
|
|||
FileJson,
|
||||
FileText,
|
||||
Globe,
|
||||
Grid2X2,
|
||||
Grid2X2 as Pixel,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Link,
|
||||
|
|
|
|||
|
|
@ -78,16 +78,18 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
</Row>
|
||||
<Row alignItems="center">
|
||||
<DialogTrigger>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero">
|
||||
<Icon>
|
||||
<Bookmark />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{formatMessage(labels.saveSegment)}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
{!!filters.length && (
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero">
|
||||
<Icon>
|
||||
<Bookmark />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{formatMessage(labels.saveSegment)}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 400 }}>
|
||||
{({ close }) => {
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ export const labels = defineMessages({
|
|||
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
||||
page: { id: 'label.page', defaultMessage: 'Page' },
|
||||
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
||||
entry: { id: 'label.entry', defaultMessage: 'Entry path' },
|
||||
exit: { id: 'label.exit', defaultMessage: 'Exit path' },
|
||||
entry: { id: 'label.entry', defaultMessage: 'Entry' },
|
||||
exit: { id: 'label.exit', defaultMessage: 'Exit' },
|
||||
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
||||
screens: { id: 'label.screens', defaultMessage: 'Screens' },
|
||||
browsers: { id: 'label.browsers', defaultMessage: 'Browsers' },
|
||||
|
|
@ -354,6 +354,8 @@ export const labels = defineMessages({
|
|||
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
||||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
||||
weekly: { id: 'label.weekly', defaultMessage: 'Weekly' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { MetricsTable, MetricsTableProps } from '@/components/metrics/MetricsTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useFormat } from '@/components/hooks';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
|
||||
export function BrowsersTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatBrowser } = useFormat();
|
||||
|
||||
function renderLink({ x: browser }) {
|
||||
return (
|
||||
<FilterLink id="browser" value={browser} label={formatBrowser(browser)}>
|
||||
<TypeIcon type="browser" value={browser} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.browsers)}
|
||||
type="browser"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from '@/components/metrics/MetricsTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function ChannelsTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const renderLabel = ({ x }) => {
|
||||
return formatMessage(labels[x]);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.channels)}
|
||||
type="channel"
|
||||
renderLabel={renderLabel}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { emptyFilter } from '@/lib/filters';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useFormat } from '@/components/hooks';
|
||||
|
||||
export function CitiesTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatCity } = useFormat();
|
||||
|
||||
const renderLink = ({ x: city, country }) => {
|
||||
return (
|
||||
<FilterLink id="city" value={city} label={formatCity(city, country)}>
|
||||
{country && (
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/country/${
|
||||
country?.toLowerCase() || 'xx'
|
||||
}.png`}
|
||||
alt={country}
|
||||
/>
|
||||
)}
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.cities)}
|
||||
type="city"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
dataFilter={emptyFilter}
|
||||
renderLabel={renderLink}
|
||||
searchFormattedValues={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,17 +3,22 @@ import { useCountryNames } from '@/components/hooks';
|
|||
import { useLocale, useMessages, useFormat } from '@/components/hooks';
|
||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable';
|
||||
|
||||
export function CountriesTable({ ...props }: MetricsTableProps) {
|
||||
export interface CountriesTableProps extends MetricsTableProps {
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export function CountriesTable({ isExpanded, ...props }: CountriesTableProps) {
|
||||
const { locale } = useLocale();
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatCountry } = useFormat();
|
||||
|
||||
const renderLink = ({ x: code }) => {
|
||||
const renderLabel = ({ label: code }) => {
|
||||
return (
|
||||
<FilterLink
|
||||
id="country"
|
||||
type="country"
|
||||
value={(countryNames[code] && code) || code}
|
||||
label={formatCountry(code)}
|
||||
>
|
||||
|
|
@ -22,13 +27,15 @@ export function CountriesTable({ ...props }: MetricsTableProps) {
|
|||
);
|
||||
};
|
||||
|
||||
const Component = isExpanded ? MetricsExpandedTable : MetricsTable;
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
<Component
|
||||
{...props}
|
||||
title={formatMessage(labels.countries)}
|
||||
type="country"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
renderLabel={renderLabel}
|
||||
searchFormattedValues={true}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useFormat } from '@/components/hooks';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
|
||||
export function DevicesTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatDevice } = useFormat();
|
||||
|
||||
function renderLink({ x: device }) {
|
||||
return (
|
||||
<FilterLink id="device" value={labels[device] && device} label={formatDevice(device)}>
|
||||
<TypeIcon type="device" value={device?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.devices)}
|
||||
type="device"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
searchFormattedValues={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export interface EventsTableProps extends MetricsTableProps {
|
||||
onLabelClick?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function EventsTable({ onLabelClick, ...props }: EventsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const renderLabel = ({ x: label }) => {
|
||||
if (onLabelClick) {
|
||||
return (
|
||||
<div onClick={() => onLabelClick(label)} style={{ cursor: 'pointer' }}>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.events)}
|
||||
type="event"
|
||||
metric={formatMessage(labels.actions)}
|
||||
renderLabel={renderLabel}
|
||||
allowDownload={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Flexbox } from '@umami/react-zen';
|
||||
|
||||
export function HostnamesTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const renderLink = ({ x: hostname }) => {
|
||||
return (
|
||||
<Flexbox alignItems="center">
|
||||
<FilterLink
|
||||
id="hostname"
|
||||
value={hostname}
|
||||
externalUrl={`https://${hostname}`}
|
||||
label={!hostname && formatMessage(labels.none)}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.hostname)}
|
||||
type="hostname"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { useLocale } from '@/components/hooks';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useFormat } from '@/components/hooks';
|
||||
|
||||
export function LanguagesTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { formatLanguage } = useFormat();
|
||||
|
||||
const renderLabel = ({ x }) => {
|
||||
return <div className={locale}>{formatLanguage(x)}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.languages)}
|
||||
type="language"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLabel}
|
||||
searchFormattedValues={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 300px;
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { useMessages } from '@/components/hooks';
|
|||
import { formatShortTime } from '@/lib/format';
|
||||
import { DataColumn, DataTable } from '@umami/react-zen';
|
||||
import { ReactNode } from 'react';
|
||||
import styles from './ListExpandedTable.module.css';
|
||||
|
||||
export interface ListExpandedTableProps {
|
||||
data?: any[];
|
||||
|
|
@ -15,7 +14,7 @@ export function ListExpandedTable({ data = [], title, renderLabel }: ListExpande
|
|||
|
||||
return (
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="label" label={title} align="start" className={styles.truncate}>
|
||||
<DataColumn id="label" label={title} align="start">
|
||||
{row =>
|
||||
renderLabel
|
||||
? renderLabel({ x: row?.['name'], country: row?.['country'] }, Number(row.id))
|
||||
|
|
|
|||
|
|
@ -9,13 +9,19 @@ import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
|||
|
||||
const ITEM_SIZE = 30;
|
||||
|
||||
interface ListData {
|
||||
label: string;
|
||||
count: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export interface ListTableProps {
|
||||
data?: any[];
|
||||
data?: ListData[];
|
||||
title?: string;
|
||||
metric?: string;
|
||||
className?: string;
|
||||
renderLabel?: (row: any, index: number) => ReactNode;
|
||||
renderChange?: (row: any, index: number) => ReactNode;
|
||||
renderLabel?: (data: ListData, index: number) => ReactNode;
|
||||
renderChange?: (data: ListData, index: number) => ReactNode;
|
||||
animate?: boolean;
|
||||
virtualize?: boolean;
|
||||
showPercentage?: boolean;
|
||||
|
|
@ -37,14 +43,14 @@ export function ListTable({
|
|||
}: ListTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const getRow = (row: { x: any; y: any; z: any }, index: number) => {
|
||||
const { x: label, y: value, z: percent } = row || {};
|
||||
const getRow = (row: ListData, index: number) => {
|
||||
const { label, count, percent } = row;
|
||||
|
||||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
key={`${label}${index}`}
|
||||
label={renderLabel ? renderLabel(row, index) : (label ?? formatMessage(labels.unknown))}
|
||||
value={value}
|
||||
value={count}
|
||||
percent={percent}
|
||||
animate={animate && !virtualize}
|
||||
showPercentage={showPercentage}
|
||||
|
|
|
|||
149
src/components/metrics/MetricLabel.tsx
Normal file
149
src/components/metrics/MetricLabel.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { Row } from '@umami/react-zen';
|
||||
import {
|
||||
useCountryNames,
|
||||
useLocale,
|
||||
useMessages,
|
||||
useRegionNames,
|
||||
useFormat,
|
||||
} from '@/components/hooks';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { GROUPED_DOMAINS } from '@/lib/constants';
|
||||
|
||||
export interface MetricLabelProps {
|
||||
type: string;
|
||||
data: any;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function MetricLabel({ type, data }: MetricLabelProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue, formatCity } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const { getRegionName } = useRegionNames(locale);
|
||||
|
||||
const { label, country, domain } = data;
|
||||
const isType = ['browser', 'country', 'device', 'os'].includes(type);
|
||||
|
||||
switch (type) {
|
||||
case 'browser':
|
||||
return (
|
||||
<FilterLink
|
||||
type="browser"
|
||||
value={label}
|
||||
label={formatValue(label, 'browser')}
|
||||
icon={<TypeIcon type="browser" value={label} />}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'channel':
|
||||
return formatMessage(labels[label]);
|
||||
|
||||
case 'city':
|
||||
return (
|
||||
<FilterLink
|
||||
type="city"
|
||||
value={label}
|
||||
label={formatCity(label, country)}
|
||||
icon={
|
||||
country && (
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/country/${
|
||||
country?.toLowerCase() || 'xx'
|
||||
}.png`}
|
||||
alt={country}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'region':
|
||||
return (
|
||||
<FilterLink
|
||||
type="region"
|
||||
value={label}
|
||||
label={getRegionName(label, country)}
|
||||
icon={<TypeIcon type="country" value={country} />}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'country':
|
||||
return (
|
||||
<FilterLink
|
||||
type="country"
|
||||
value={(countryNames[label] && label) || label}
|
||||
label={formatValue(label, 'country')}
|
||||
icon={<TypeIcon type="country" value={label} />}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'path':
|
||||
case 'entry':
|
||||
case 'exit':
|
||||
return (
|
||||
<FilterLink
|
||||
type={type === 'entry' || type === 'exit' ? 'path' : type}
|
||||
value={label}
|
||||
label={!label && formatMessage(labels.none)}
|
||||
externalUrl={
|
||||
domain ? `${domain?.startsWith('http') ? domain : `https://${domain}`}${label}` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'device':
|
||||
return (
|
||||
<FilterLink
|
||||
type="device"
|
||||
value={labels[label] && label}
|
||||
label={formatValue(label, 'device')}
|
||||
icon={<TypeIcon type="device" value={label?.toLowerCase()} />}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'referrer':
|
||||
return (
|
||||
<FilterLink
|
||||
type="referrer"
|
||||
value={label}
|
||||
externalUrl={`https://${label}`}
|
||||
label={!label && formatMessage(labels.none)}
|
||||
icon={<Favicon domain={label} />}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'grouped':
|
||||
if (label === 'Other') {
|
||||
return `(${formatMessage(labels.other)})`;
|
||||
} else {
|
||||
return (
|
||||
<Row alignItems="center" gap="3">
|
||||
<Favicon domain={label} />
|
||||
{GROUPED_DOMAINS.find(({ domain }) => domain === label)?.name}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
case 'language':
|
||||
return formatValue(label, 'language');
|
||||
|
||||
default:
|
||||
return (
|
||||
<FilterLink
|
||||
type={type}
|
||||
value={label}
|
||||
icon={
|
||||
isType && (
|
||||
<TypeIcon
|
||||
type={type as 'browser' | 'country' | 'device' | 'os'}
|
||||
value={label?.toLowerCase()?.replaceAll(/\W/g, '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
106
src/components/metrics/MetricsExpandedTable.tsx
Normal file
106
src/components/metrics/MetricsExpandedTable.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { ReactNode, useState } from 'react';
|
||||
import { Button, Column, DataColumn, DataTable, Icon, Row, SearchField } from '@umami/react-zen';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useWebsiteExpandedMetricsQuery } from '@/components/hooks';
|
||||
import { Close } from '@/components/icons';
|
||||
import { DownloadButton } from '@/components/input/DownloadButton';
|
||||
import { formatShortTime } from '@/lib/format';
|
||||
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
||||
|
||||
export interface MetricsExpandedTableProps {
|
||||
websiteId: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
dataFilter?: (data: any) => any;
|
||||
onSearch?: (search: string) => void;
|
||||
params?: { [key: string]: any };
|
||||
allowSearch?: boolean;
|
||||
allowDownload?: boolean;
|
||||
renderLabel?: (row: any, index: number) => ReactNode;
|
||||
onClose?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function MetricsExpandedTable({
|
||||
websiteId,
|
||||
type,
|
||||
title,
|
||||
params,
|
||||
allowSearch = true,
|
||||
allowDownload = true,
|
||||
onClose,
|
||||
children,
|
||||
}: MetricsExpandedTableProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const isType = ['browser', 'country', 'device', 'os'].includes(type);
|
||||
|
||||
const { data, isLoading, isFetching, error } = useWebsiteExpandedMetricsQuery(websiteId, {
|
||||
type,
|
||||
search: isType ? undefined : search,
|
||||
...params,
|
||||
});
|
||||
|
||||
const items = data?.map(({ name, ...props }) => ({ label: name, ...props }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row alignItems="center" paddingBottom="3">
|
||||
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
|
||||
<Row justifyContent="flex-end" flexGrow={1} gap>
|
||||
{children}
|
||||
{allowDownload && <DownloadButton filename={type} data={data} />}
|
||||
{onClose && (
|
||||
<Button onPress={onClose} variant="quiet">
|
||||
<Icon>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
<LoadingPanel
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
height="100%"
|
||||
>
|
||||
<Column overflowY="auto" minHeight="0" height="100%" paddingRight="3" overflow="hidden">
|
||||
{items && (
|
||||
<DataTable data={items}>
|
||||
<DataColumn id="label" label={title} width="2fr" align="start">
|
||||
{row => <MetricLabel type={type} data={row} />}
|
||||
</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="pageviews" label={formatMessage(labels.views)} align="end">
|
||||
{row => row?.['pageviews']?.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']) * 100;
|
||||
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
)}
|
||||
</Column>
|
||||
</LoadingPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,37 +1,20 @@
|
|||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { Button, Column, Icon, Row, SearchField, Text } from '@umami/react-zen';
|
||||
import { useMemo } from 'react';
|
||||
import { Icon, Row, Text } from '@umami/react-zen';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import {
|
||||
useFormat,
|
||||
useMessages,
|
||||
useNavigation,
|
||||
useWebsiteExpandedMetricsQuery,
|
||||
useWebsiteMetricsQuery,
|
||||
} from '@/components/hooks';
|
||||
import { Close, Maximize } from '@/components/icons';
|
||||
import { DownloadButton } from '@/components/input/DownloadButton';
|
||||
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
|
||||
import { useMessages, useNavigation, useWebsiteMetricsQuery } from '@/components/hooks';
|
||||
import { Maximize } from '@/components/icons';
|
||||
import { percentFilter } from '@/lib/filters';
|
||||
|
||||
import { ListExpandedTable, ListExpandedTableProps } from './ListExpandedTable';
|
||||
import { ListTable, ListTableProps } from './ListTable';
|
||||
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
||||
|
||||
export interface MetricsTableProps extends ListTableProps {
|
||||
websiteId: string;
|
||||
type?: string;
|
||||
type: string;
|
||||
dataFilter?: (data: any) => any;
|
||||
limit?: number;
|
||||
delay?: number;
|
||||
onSearch?: (search: string) => void;
|
||||
allowSearch?: boolean;
|
||||
searchFormattedValues?: boolean;
|
||||
showMore?: boolean;
|
||||
params?: { [key: string]: any };
|
||||
allowDownload?: boolean;
|
||||
isExpanded?: boolean;
|
||||
onClose?: () => void;
|
||||
children?: ReactNode;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function MetricsTable({
|
||||
|
|
@ -39,50 +22,17 @@ export function MetricsTable({
|
|||
type,
|
||||
dataFilter,
|
||||
limit,
|
||||
delay = null,
|
||||
allowSearch = false,
|
||||
searchFormattedValues = false,
|
||||
showMore = true,
|
||||
showMore = false,
|
||||
params,
|
||||
allowDownload = true,
|
||||
isExpanded = false,
|
||||
onClose,
|
||||
children,
|
||||
...props
|
||||
}: MetricsTableProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatValue } = useFormat();
|
||||
const { updateParams } = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const expandedQuery = useWebsiteExpandedMetricsQuery(
|
||||
websiteId,
|
||||
{
|
||||
type,
|
||||
search: searchFormattedValues ? undefined : search,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
enabled: isExpanded,
|
||||
},
|
||||
);
|
||||
|
||||
const query = useWebsiteMetricsQuery(
|
||||
websiteId,
|
||||
{
|
||||
type,
|
||||
limit,
|
||||
search: searchFormattedValues ? undefined : search,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
enabled: !isExpanded,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, error } = isExpanded ? expandedQuery : query;
|
||||
const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(websiteId, {
|
||||
type,
|
||||
limit,
|
||||
...params,
|
||||
});
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
|
|
@ -98,23 +48,16 @@ export function MetricsTable({
|
|||
}
|
||||
}
|
||||
|
||||
if (searchFormattedValues && search) {
|
||||
items = items.filter(({ x, ...data }) => {
|
||||
const value = formatValue(x, type, data);
|
||||
|
||||
return value?.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
items = percentFilter(items);
|
||||
|
||||
return items;
|
||||
return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
|
||||
}
|
||||
return [];
|
||||
}, [data, dataFilter, search, limit, formatValue, type]);
|
||||
}, [data, dataFilter, limit, type]);
|
||||
|
||||
const downloadData = isExpanded ? data : filteredData;
|
||||
const hasActions = data && (allowSearch || allowDownload || onClose || children);
|
||||
const renderLabel = (data: any) => {
|
||||
return <MetricLabel type={type} data={data} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
|
|
@ -124,37 +67,7 @@ export function MetricsTable({
|
|||
error={error}
|
||||
height="100%"
|
||||
>
|
||||
{hasActions && (
|
||||
<Row alignItems="center" paddingBottom="3">
|
||||
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
|
||||
<Row justifyContent="flex-end" flexGrow={1} gap>
|
||||
{children}
|
||||
{allowDownload && <DownloadButton filename={type} data={downloadData} />}
|
||||
{onClose && (
|
||||
<Button onPress={onClose} variant="quiet">
|
||||
<Icon>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
)}
|
||||
<Column
|
||||
overflowY="auto"
|
||||
minHeight="0"
|
||||
height="100%"
|
||||
paddingRight="3"
|
||||
overflow="hidden"
|
||||
flexGrow={1}
|
||||
>
|
||||
{data &&
|
||||
(isExpanded ? (
|
||||
<ListExpandedTable {...(props as ListExpandedTableProps)} data={data} />
|
||||
) : (
|
||||
<ListTable {...(props as ListTableProps)} data={filteredData} />
|
||||
))}
|
||||
</Column>
|
||||
{data && <ListTable {...props} data={filteredData} renderLabel={renderLabel} />}
|
||||
{showMore && limit && (
|
||||
<Row justifyContent="center">
|
||||
<LinkButton href={updateParams({ view: type })} variant="quiet">
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages, useFormat } from '@/components/hooks';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
|
||||
export function OSTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatOS } = useFormat();
|
||||
|
||||
function renderLink({ x: os }) {
|
||||
return (
|
||||
<FilterLink id="os" value={os} label={formatOS(os)}>
|
||||
<TypeIcon type="os" value={os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
type="os"
|
||||
title={formatMessage(labels.os)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||
import { emptyFilter } from '@/lib/filters';
|
||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
|
||||
export interface PagesTableProps extends MetricsTableProps {
|
||||
type: string;
|
||||
allowFilter?: boolean;
|
||||
}
|
||||
|
||||
export function PagesTable({ type, allowFilter, ...props }: PagesTableProps) {
|
||||
const { router, updateParams } = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { domain } = useWebsite();
|
||||
|
||||
const handleChange = (id: any) => {
|
||||
router.push(updateParams({ view: id }));
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'path',
|
||||
label: formatMessage(labels.path),
|
||||
},
|
||||
{
|
||||
id: 'entry',
|
||||
label: formatMessage(labels.entry),
|
||||
},
|
||||
{
|
||||
id: 'exit',
|
||||
label: formatMessage(labels.exit),
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
label: formatMessage(labels.title),
|
||||
},
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
return (
|
||||
<FilterLink
|
||||
id={type === 'entry' || type === 'exit' ? 'path' : type}
|
||||
value={x}
|
||||
label={!x && formatMessage(labels.none)}
|
||||
externalUrl={
|
||||
type !== 'title'
|
||||
? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.pages)}
|
||||
type={type}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
dataFilter={emptyFilter}
|
||||
renderLabel={renderLink}
|
||||
>
|
||||
{allowFilter && <FilterButtons items={buttons} value={type} onChange={handleChange} />}
|
||||
</MetricsTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.param {
|
||||
padding: 0 8px;
|
||||
color: var(--primary-color);
|
||||
background: var(--blue100);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Row, Text } from '@umami/react-zen';
|
||||
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||
import { emptyFilter, paramFilter } from '@/lib/filters';
|
||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
const FILTER_COMBINED = 'filter-combined';
|
||||
const FILTER_RAW = 'filter-raw';
|
||||
|
||||
const filters = {
|
||||
[FILTER_RAW]: emptyFilter,
|
||||
[FILTER_COMBINED]: [emptyFilter, paramFilter],
|
||||
};
|
||||
|
||||
export function QueryParametersTable({
|
||||
allowFilter,
|
||||
...props
|
||||
}: { allowFilter?: boolean } & MetricsTableProps) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: FILTER_COMBINED,
|
||||
label: formatMessage(labels.filterCombined),
|
||||
},
|
||||
{ id: FILTER_RAW, label: formatMessage(labels.filterRaw) },
|
||||
];
|
||||
|
||||
const renderLabel = ({ x, p, v }) => {
|
||||
return (
|
||||
<Row alignItems="center" maxWidth="600px" gap>
|
||||
{filter === FILTER_RAW ? (
|
||||
<Text truncate title={x}>
|
||||
{x}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text color="primary" weight="bold">
|
||||
{p}
|
||||
</Text>
|
||||
<Text truncate title={v}>
|
||||
{v}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.query)}
|
||||
type="query"
|
||||
metric={formatMessage(labels.views)}
|
||||
dataFilter={filters[filter]}
|
||||
renderLabel={renderLabel}
|
||||
delay={0}
|
||||
isExpanded={false}
|
||||
>
|
||||
{allowFilter && <FilterButtons items={buttons} value={filter} onChange={setFilter} />}
|
||||
</MetricsTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { GROUPED_DOMAINS } from '@/lib/constants';
|
||||
import { emptyFilter } from '@/lib/filters';
|
||||
import { Row } from '@umami/react-zen';
|
||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
|
||||
export interface ReferrersTableProps extends MetricsTableProps {
|
||||
allowFilter?: boolean;
|
||||
}
|
||||
|
||||
export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
|
||||
const {
|
||||
router,
|
||||
updateParams,
|
||||
query: { view = 'referrer' },
|
||||
} = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSelect = (key: any) => {
|
||||
router.push(updateParams({ view: key }));
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'referrer',
|
||||
label: formatMessage(labels.domain),
|
||||
},
|
||||
{
|
||||
id: 'grouped',
|
||||
label: formatMessage(labels.grouped),
|
||||
},
|
||||
];
|
||||
|
||||
const renderLink = ({ x: referrer }) => {
|
||||
if (view === 'grouped') {
|
||||
if (referrer === 'Other') {
|
||||
return `(${formatMessage(labels.other)})`;
|
||||
} else {
|
||||
return (
|
||||
<Row alignItems="center" gap="3">
|
||||
<Favicon domain={referrer} />
|
||||
{GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterLink
|
||||
id="referrer"
|
||||
value={referrer}
|
||||
externalUrl={`https://${referrer}`}
|
||||
label={!referrer && formatMessage(labels.none)}
|
||||
>
|
||||
<Favicon domain={referrer} />
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.referrers)}
|
||||
type={view}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
dataFilter={emptyFilter}
|
||||
renderLabel={renderLink}
|
||||
>
|
||||
{allowFilter && <FilterButtons items={buttons} value={view} onChange={handleSelect} />}
|
||||
</MetricsTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { emptyFilter } from '@/lib/filters';
|
||||
import { useMessages, useLocale, useRegionNames } from '@/components/hooks';
|
||||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
|
||||
export function RegionsTable(props: MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { getRegionName } = useRegionNames(locale);
|
||||
|
||||
const renderLink = ({ x: code, country }) => {
|
||||
return (
|
||||
<FilterLink id="region" value={code} label={getRegionName(code, country)}>
|
||||
<TypeIcon type="country" value={country?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.regions)}
|
||||
type="region"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
dataFilter={emptyFilter}
|
||||
renderLabel={renderLink}
|
||||
searchFormattedValues={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function ScreenTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.screens)}
|
||||
type="screen"
|
||||
metric={formatMessage(labels.visitors)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { MetricsTable, MetricsTableProps } from './MetricsTable';
|
||||
import { FilterLink } from '@/components/common/FilterLink';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Flexbox } from '@umami/react-zen';
|
||||
|
||||
export function TagsTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const renderLink = ({ x: tag }) => {
|
||||
return (
|
||||
<Flexbox alignItems="center">
|
||||
<FilterLink id="tag" value={tag} label={!tag && formatMessage(labels.none)} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.tags)}
|
||||
type="tag"
|
||||
metric={formatMessage(labels.views)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
112
src/components/metrics/WeeklyTraffic.tsx
Normal file
112
src/components/metrics/WeeklyTraffic.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { Row, Grid, Text } from '@umami/react-zen';
|
||||
import { format, startOfDay, addHours } from 'date-fns';
|
||||
import { useLocale, useMessages, useWeeklyTrafficQuery } from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { getDayOfWeekAsDate } from '@/lib/date';
|
||||
import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
|
||||
export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
|
||||
const { data, isLoading, error } = useWeeklyTrafficQuery(websiteId);
|
||||
const { dateLocale } = useLocale();
|
||||
const { labels, formatMessage } = useMessages();
|
||||
const { weekStartsOn } = dateLocale.options;
|
||||
const daysOfWeek = Array(7)
|
||||
.fill(weekStartsOn)
|
||||
.map((d, i) => (d + i) % 7);
|
||||
|
||||
const [, max = 1] = data
|
||||
? data.reduce((arr: number[], hours: number[], index: number) => {
|
||||
const min = Math.min(...hours);
|
||||
const max = Math.max(...hours);
|
||||
|
||||
if (index === 0) {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
if (min < arr[0]) {
|
||||
arr[0] = min;
|
||||
}
|
||||
|
||||
if (max > arr[1]) {
|
||||
arr[1] = max;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [])
|
||||
: [];
|
||||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<Grid columns="repeat(8, 1fr)" gap>
|
||||
{data && (
|
||||
<>
|
||||
<Grid rows="repeat(25, 16px)" gap="1">
|
||||
<Row> </Row>
|
||||
{Array(24)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
const label = format(addHours(startOfDay(new Date()), i), 'haaa', {
|
||||
locale: dateLocale,
|
||||
});
|
||||
return (
|
||||
<Row key={i} justifyContent="flex-end">
|
||||
<Text color="muted" size="2">
|
||||
{label}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
{daysOfWeek.map((index: number) => {
|
||||
const day = data[index];
|
||||
return (
|
||||
<Grid
|
||||
rows="repeat(24, 16px)"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
key={index}
|
||||
gap="1"
|
||||
>
|
||||
<Row alignItems="center" justifyContent="center" marginBottom="3">
|
||||
<Text weight="bold" align="center">
|
||||
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
|
||||
</Text>
|
||||
</Row>
|
||||
{day?.map((count: number, j) => {
|
||||
const pct = max ? count / max : 0;
|
||||
return (
|
||||
<TooltipTrigger key={j} delay={0} isDisabled={count <= 0}>
|
||||
<Focusable>
|
||||
<Row
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor="2"
|
||||
width="16px"
|
||||
height="16px"
|
||||
borderRadius="full"
|
||||
style={{ margin: '0 auto' }}
|
||||
>
|
||||
<Row
|
||||
backgroundColor="primary"
|
||||
width="16px"
|
||||
height="16px"
|
||||
borderRadius="full"
|
||||
style={{ opacity: pct, transform: `scale(${pct})` }}
|
||||
/>
|
||||
</Row>
|
||||
</Focusable>
|
||||
<Tooltip placement="right">{`${formatMessage(
|
||||
labels.visitors,
|
||||
)}: ${count}`}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue