Merge branch 'dev' into search-formatted-metrics

This commit is contained in:
Mike Cao 2024-11-28 16:36:29 -08:00 committed by GitHub
commit 4ab8b1ff91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
807 changed files with 45367 additions and 8474 deletions

View file

@ -2,6 +2,7 @@ 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();
@ -10,12 +11,7 @@ export function BrowsersTable(props: MetricsTableProps) {
function renderLink({ x: browser }) {
return (
<FilterLink id="browser" value={browser} label={formatBrowser(browser)}>
<img
src={`${process.env.basePath}/images/browsers/${browser || 'unknown'}.png`}
alt={browser}
width={16}
height={16}
/>
<TypeIcon type="browser" value={browser} />
</FilterLink>
);
}

View file

@ -0,0 +1,26 @@
.label {
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
font-weight: 700;
padding: 0.1em 0.5em;
border-radius: 5px;
color: var(--base500);
align-self: flex-start;
}
.positive {
color: var(--green700);
background: var(--green100);
}
.negative {
color: var(--red700);
background: var(--red100);
}
.neutral {
color: var(--base700);
background: var(--base100);
}

View file

@ -0,0 +1,46 @@
import classNames from 'classnames';
import { Icon, Icons } from 'react-basics';
import { ReactNode } from 'react';
import styles from './ChangeLabel.module.css';
export function ChangeLabel({
value,
size,
title,
reverseColors,
className,
children,
}: {
value: number;
size?: 'xs' | 'sm' | 'md' | 'lg';
title?: string;
reverseColors?: boolean;
showPercentage?: boolean;
className?: string;
children?: ReactNode;
}) {
const positive = value >= 0;
const negative = value < 0;
const neutral = value === 0 || isNaN(value);
const good = reverseColors ? negative : positive;
return (
<div
className={classNames(styles.label, className, {
[styles.positive]: good,
[styles.negative]: !good,
[styles.neutral]: neutral,
})}
title={title}
>
{!neutral && (
<Icon rotate={positive ? -90 : 90} size={size}>
<Icons.ArrowRight />
</Icon>
)}
{children || value}
</div>
);
}
export default ChangeLabel;

View file

@ -1,6 +1,9 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { emptyFilter } from 'lib/filters';
import FilterLink from 'components/common/FilterLink';
import TypeIcon from 'components/common/TypeIcon';
import { useLocale } from 'components/hooks';
import { useMessages } from 'components/hooks';
import { useFormat } from 'components/hooks';

View file

@ -2,34 +2,22 @@ import FilterLink from 'components/common/FilterLink';
import { useCountryNames } from 'components/hooks';
import { useLocale, useMessages, useFormat } from 'components/hooks';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import TypeIcon from 'components/common/TypeIcon';
export function CountriesTable({
onDataLoad,
...props
}: {
onDataLoad: (data: any) => void;
} & MetricsTableProps) {
export function CountriesTable({ ...props }: MetricsTableProps) {
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
const { countryNames } = useCountryNames(locale);
const { formatMessage, labels } = useMessages();
const { formatCountry } = useFormat();
const handleDataLoad = (data: any) => {
onDataLoad?.(data);
};
const renderLink = ({ x: code }) => {
return (
<FilterLink
id="country"
className={locale}
value={countryNames[code] && code}
value={(countryNames[code] && code) || code}
label={formatCountry(code)}
>
<img
src={`${process.env.basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
alt={code}
/>
<TypeIcon type="country" value={code?.toLowerCase()} />
</FilterLink>
);
};

View file

@ -2,6 +2,7 @@ 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();
@ -10,12 +11,7 @@ export function DevicesTable(props: MetricsTableProps) {
function renderLink({ x: device }) {
return (
<FilterLink id="device" value={labels[device] && device} label={formatDevice(device)}>
<img
src={`${process.env.basePath}/images/device/${device?.toLowerCase() || 'unknown'}.png`}
alt={device}
width={16}
height={16}
/>
<TypeIcon type="device" value={device?.toLowerCase()} />
</FilterLink>
);
}

View file

@ -1,11 +1,9 @@
import { useMemo } from 'react';
import { Loading } from 'react-basics';
import { colord } from 'colord';
import BarChart from 'components/charts/BarChart';
import { getDateArray } from 'lib/date';
import { useLocale, useDateRange, useWebsiteEvents } from 'components/hooks';
import { CHART_COLORS } from 'lib/constants';
import { useDateRange, useLocale, useWebsiteEventsSeries } from 'components/hooks';
import { renderDateLabels } from 'lib/charts';
import { CHART_COLORS } from 'lib/constants';
import { useMemo } from 'react';
export interface EventsChartProps {
websiteId: string;
@ -13,9 +11,11 @@ export interface EventsChartProps {
}
export function EventsChart({ websiteId, className }: EventsChartProps) {
const [{ startDate, endDate, unit }] = useDateRange(websiteId);
const {
dateRange: { startDate, endDate, unit, value },
} = useDateRange(websiteId);
const { locale } = useLocale();
const { data, isLoading } = useWebsiteEvents(websiteId);
const { data, isLoading } = useWebsiteEventsSeries(websiteId);
const chartData = useMemo(() => {
if (!data) return [];
@ -30,10 +30,6 @@ export function EventsChart({ websiteId, className }: EventsChartProps) {
return obj;
}, {});
Object.keys(map).forEach(key => {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
return {
datasets: Object.keys(map).map((key, index) => {
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
@ -49,18 +45,17 @@ export function EventsChart({ websiteId, className }: EventsChartProps) {
};
}, [data, startDate, endDate, unit]);
if (isLoading) {
return <Loading icon="dots" />;
}
return (
<BarChart
minDate={startDate.toISOString()}
maxDate={endDate.toISOString()}
className={className}
data={chartData}
unit={unit}
stacked={true}
renderXLabel={renderDateLabels(unit, locale)}
isLoading={isLoading}
isAllTime={value === 'all'}
/>
);
}

View file

@ -2,6 +2,12 @@
display: flex;
align-items: center;
gap: 10px;
background: var(--base75);
padding: 10px 20px;
border: 1px solid var(--base400);
border-radius: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.label {
@ -12,12 +18,13 @@
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
background: var(--base75);
gap: 4px;
font-size: 12px;
background: var(--base50);
border: 1px solid var(--base400);
border-radius: var(--border-radius);
box-shadow: 1px 1px 1px var(--base500);
padding: 8px 16px;
padding: 6px 14px;
cursor: pointer;
}
@ -27,6 +34,8 @@
.close {
font-weight: 700;
align-self: center;
margin-left: auto;
}
.name,

View file

@ -13,6 +13,7 @@ import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditFo
import { OPERATOR_PREFIXES } from 'lib/constants';
import { isSearchOperator, parseParameterValue } from 'lib/params';
import styles from './FilterTags.module.css';
import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton';
export function FilterTags({
websiteId,
@ -23,7 +24,7 @@ export function FilterTags({
}) {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
const {
router,
renderUrl,
@ -100,6 +101,7 @@ export function FilterTags({
</PopupTrigger>
);
})}
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
<Button className={styles.close} variant="quiet" onClick={handleResetFilter}>
<Icon>
<Icons.Close />

View file

@ -0,0 +1,35 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import { Flexbox } from 'react-basics';
export function HostsTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const renderLink = ({ x: host }) => {
return (
<Flexbox alignItems="center">
<FilterLink
id="host"
value={host}
externalUrl={`https://${host}`}
label={!host && formatMessage(labels.none)}
/>
</Flexbox>
);
};
return (
<>
<MetricsTable
{...props}
title={formatMessage(labels.hosts)}
type="host"
metric={formatMessage(labels.visitors)}
renderLabel={renderLink}
/>
</>
);
}
export default HostsTable;

View file

@ -3,7 +3,6 @@ import { safeDecodeURIComponent } from 'next-basics';
import { colord } from 'colord';
import classNames from 'classnames';
import { LegendItem } from 'chart.js/auto';
import { useLocale } from 'components/hooks';
import styles from './Legend.module.css';
export function Legend({
@ -13,8 +12,6 @@ export function Legend({
items: any[];
onClick: (index: LegendItem) => void;
}) {
const { locale } = useLocale();
if (!items.find(({ text }) => text)) {
return null;
}
@ -32,7 +29,7 @@ export function Legend({
onClick={() => onClick(item)}
>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
<span className={locale}>{safeDecodeURIComponent(text)}</span>
{safeDecodeURIComponent(text)}
</StatusLight>
</div>
);

View file

@ -72,9 +72,11 @@
}
.value {
width: 50px;
display: flex;
align-items: center;
gap: 10px;
text-align: end;
margin-inline-end: 10px;
margin-inline-end: 5px;
font-weight: 600;
}

View file

@ -14,7 +14,8 @@ export interface ListTableProps {
title?: string;
metric?: string;
className?: string;
renderLabel?: (row: any) => ReactNode;
renderLabel?: (row: any, index: number) => ReactNode;
renderChange?: (row: any, index: number) => ReactNode;
animate?: boolean;
virtualize?: boolean;
showPercentage?: boolean;
@ -27,6 +28,7 @@ export function ListTable({
metric,
className,
renderLabel,
renderChange,
animate = true,
virtualize = false,
showPercentage = true,
@ -34,23 +36,24 @@ export function ListTable({
}: ListTableProps) {
const { formatMessage, labels } = useMessages();
const getRow = row => {
const getRow = (row: { x: any; y: any; z: any }, index: number) => {
const { x: label, y: value, z: percent } = row;
return (
<AnimatedRow
key={label}
label={renderLabel ? renderLabel(row) : label ?? formatMessage(labels.unknown)}
label={renderLabel ? renderLabel(row, index) : label ?? formatMessage(labels.unknown)}
value={value}
percent={percent}
animate={animate && !virtualize}
showPercentage={showPercentage}
change={renderChange ? renderChange(row, index) : null}
/>
);
};
const Row = ({ index, style }) => {
return <div style={style}>{getRow(data[index])}</div>;
return <div style={style}>{getRow(data[index], index)}</div>;
};
return (
@ -71,14 +74,14 @@ export function ListTable({
{Row}
</FixedSizeList>
) : (
data.map(row => getRow(row))
data.map(getRow)
)}
</div>
</div>
);
}
const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true }) => {
const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentage = true }) => {
const props = useSpring({
width: percent,
y: value,
@ -90,6 +93,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true
<div className={styles.row}>
<div className={styles.label}>{label}</div>
<div className={styles.value}>
{change}
<animated.div className={styles.value} title={props?.y as any}>
{props.y?.to(formatLongNumber)}
</animated.div>
@ -97,9 +101,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true
{showPercentage && (
<div className={styles.percent}>
<animated.div className={styles.bar} style={{ width: props.width.to(n => `${n}%`) }} />
<animated.span className={styles.percentValue}>
{props.width.to(n => `${n?.toFixed?.(0)}%`)}
</animated.span>
<animated.span>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</animated.span>
</div>
)}
</div>

View file

@ -2,47 +2,36 @@
display: flex;
flex-direction: column;
justify-content: center;
min-height: 90px;
min-width: 140px;
min-width: 150px;
}
.card.compare .change {
font-size: 16px;
margin: 10px 0;
}
.card:first-child {
padding-left: 0;
}
.card:last-child {
border: 0;
}
.value {
display: flex;
align-items: center;
font-size: 36px;
font-weight: 700;
white-space: nowrap;
min-height: 60px;
color: var(--base900);
line-height: 1.5;
}
.label {
display: flex;
align-items: center;
font-weight: 700;
gap: 10px;
white-space: nowrap;
min-height: 30px;
.value.prev {
color: var(--base800);
}
.change {
font-size: 12px;
padding: 0 5px;
border-radius: 5px;
color: var(--base500);
}
.change.positive {
color: var(--green700);
background: var(--green100);
}
.change.negative {
color: var(--red700);
background: var(--red100);
}
.change.plusSign::before {
content: '+';
.label {
font-weight: 700;
white-space: nowrap;
color: var(--base800);
}

View file

@ -1,15 +1,19 @@
import classNames from 'classnames';
import { useSpring, animated } from '@react-spring/web';
import { formatNumber } from 'lib/format';
import ChangeLabel from 'components/metrics/ChangeLabel';
import styles from './MetricCard.module.css';
export interface MetricCardProps {
value: number;
previousValue?: number;
change?: number;
label: string;
label?: string;
reverseColors?: boolean;
format?: typeof formatNumber;
hideComparison?: boolean;
formatValue?: (n: any) => string;
showLabel?: boolean;
showChange?: boolean;
showPrevious?: boolean;
className?: string;
}
@ -18,33 +22,39 @@ export const MetricCard = ({
change = 0,
label,
reverseColors = false,
format = formatNumber,
hideComparison = false,
formatValue = formatNumber,
showLabel = true,
showChange = false,
showPrevious = false,
className,
}: MetricCardProps) => {
const diff = value - change;
const pct = ((value - diff) / diff) * 100;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return (
<div className={classNames(styles.card, className)}>
<animated.div className={styles.value} title={props?.x as any}>
{props?.x?.to(x => format(x))}
<div className={classNames(styles.card, className, showPrevious && styles.compare)}>
{showLabel && <div className={styles.label}>{label}</div>}
<animated.div className={styles.value} title={value?.toString()}>
{props?.x?.to(x => formatValue(x))}
</animated.div>
<div className={styles.label}>
{label}
{~~change !== 0 && !hideComparison && (
<animated.span
className={classNames(styles.change, {
[styles.positive]: change * (reverseColors ? -1 : 1) >= 0,
[styles.negative]: change * (reverseColors ? -1 : 1) < 0,
[styles.plusSign]: change > 0,
})}
title={changeProps?.x as any}
>
{changeProps?.x?.to(x => format(x))}
</animated.span>
)}
</div>
{showChange && (
<ChangeLabel
className={styles.change}
value={change}
title={formatValue(change)}
reverseColors={reverseColors}
>
<animated.span>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</animated.span>
</ChangeLabel>
)}
{showPrevious && (
<animated.div className={classNames(styles.value, styles.prev)} title={diff.toString()}>
{prevProps?.x?.to(x => formatValue(x))}
</animated.div>
)}
</div>
);
};

View file

@ -2,6 +2,8 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
gap: 20px;
width: 100%;
position: relative;
}
@media screen and (max-width: 768px) {

View file

@ -6,7 +6,6 @@ import LinkButton from 'components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { percentFilter } from 'lib/filters';
import {
useDateRange,
useNavigation,
useWebsiteMetrics,
useMessages,
@ -19,7 +18,6 @@ import styles from './MetricsTable.module.css';
export interface MetricsTableProps extends ListTableProps {
websiteId: string;
domainName: string;
type?: string;
className?: string;
dataFilter?: (data: any) => any;
@ -29,6 +27,8 @@ export interface MetricsTableProps extends ListTableProps {
onSearch?: (search: string) => void;
allowSearch?: boolean;
searchFormattedValues?: boolean;
showMore?: boolean;
params?: { [key: string]: any };
children?: ReactNode;
}
@ -42,21 +42,20 @@ export function MetricsTable({
delay = null,
allowSearch = false,
searchFormattedValues = false,
showMore = true,
params,
children,
...props
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const [{ startDate, endDate }] = useDateRange(websiteId);
const {
renderUrl,
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { dir } = useLocale();
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
websiteId,
{ type, limit, search, ...params },
{
type,
startAt: +startDate,
@ -72,8 +71,9 @@ export function MetricsTable({
city,
limit,
search: (searchFormattedValues) ? undefined : search,
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
onDataLoad,
},
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
);
const filteredData = useMemo(() => {
@ -125,7 +125,7 @@ export function MetricsTable({
)}
{!data && isLoading && !isFetched && <Loading icon="dots" />}
<div className={styles.footer}>
{data && !error && limit && (
{showMore && data && !error && limit && (
<LinkButton href={renderUrl({ view: type })} variant="quiet">
<Text>{formatMessage(labels.more)}</Text>
<Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}>

View file

@ -1,6 +1,7 @@
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();
@ -9,14 +10,7 @@ export function OSTable(props: MetricsTableProps) {
function renderLink({ x: os }) {
return (
<FilterLink id="os" value={os} label={formatOS(os)}>
<img
src={`${process.env.basePath || ''}/images/os/${
os?.toLowerCase()?.replaceAll(/\W/g, '-') || 'unknown'
}.png`}
alt={os}
width={16}
height={16}
/>
<TypeIcon type="os" value={os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
</FilterLink>
);
}

View file

@ -1,31 +1,41 @@
import FilterLink from 'components/common/FilterLink';
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
import FilterButtons from 'components/common/FilterButtons';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { useMessages } from 'components/hooks';
import { useNavigation } from 'components/hooks';
import FilterLink from 'components/common/FilterLink';
import { useMessages, useNavigation } from 'components/hooks';
import { emptyFilter } from 'lib/filters';
import { useContext } from 'react';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
export interface PagesTableProps extends MetricsTableProps {
allowFilter?: boolean;
}
export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProps) {
export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const {
router,
renderUrl,
query: { view = 'url' },
} = useNavigation();
const { formatMessage, labels } = useMessages();
const { domain } = useContext(WebsiteContext);
const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }), { scroll: true });
router.push(renderUrl({ view: key }), { scroll: false });
};
const buttons = [
{
label: 'URL',
label: formatMessage(labels.path),
key: 'url',
},
{
label: formatMessage(labels.entry),
key: 'entry',
},
{
label: formatMessage(labels.exit),
key: 'exit',
},
{
label: formatMessage(labels.title),
key: 'title',
@ -35,10 +45,14 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
const renderLink = ({ x }) => {
return (
<FilterLink
id={view}
id={view === 'entry' || view === 'exit' ? 'url' : view}
value={x}
label={!x && formatMessage(labels.none)}
externalUrl={`${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`}
externalUrl={
view !== 'title'
? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}`
: null
}
/>
);
};
@ -46,7 +60,6 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
return (
<MetricsTable
{...props}
domainName={domainName}
title={formatMessage(labels.pages)}
type={view}
metric={formatMessage(labels.views)}

View file

@ -3,16 +3,27 @@ import BarChart, { BarChartProps } from 'components/charts/BarChart';
import { useLocale, useTheme, useMessages } from 'components/hooks';
import { renderDateLabels } from 'lib/charts';
export interface PageviewsChartProps extends BarChartProps {
export interface PagepageviewsChartProps extends BarChartProps {
data: {
sessions: any[];
pageviews: any[];
sessions: any[];
compare?: {
pageviews: any[];
sessions: any[];
};
};
unit: string;
isLoading?: boolean;
isAllTime?: boolean;
}
export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsChartProps) {
export function PagepageviewsChart({
data,
unit,
isLoading,
isAllTime,
...props
}: PagepageviewsChartProps) {
const { formatMessage, labels } = useMessages();
const { colors } = useTheme();
const { locale } = useLocale();
@ -29,13 +40,37 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
data: data.sessions,
borderWidth: 1,
...colors.chart.visitors,
order: 3,
},
{
label: formatMessage(labels.views),
data: data.pageviews,
borderWidth: 1,
...colors.chart.views,
order: 4,
},
...(data.compare
? [
{
type: 'line',
label: `${formatMessage(labels.views)} (${formatMessage(labels.previous)})`,
data: data.compare.pageviews,
borderWidth: 2,
backgroundColor: '#8601B0',
borderColor: '#8601B0',
order: 1,
},
{
type: 'line',
label: `${formatMessage(labels.visitors)} (${formatMessage(labels.previous)})`,
data: data.compare.sessions,
borderWidth: 2,
backgroundColor: '#f15bb5',
borderColor: '#f15bb5',
order: 2,
},
]
: []),
],
};
}, [data, locale]);
@ -46,9 +81,10 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
data={chartData}
unit={unit}
isLoading={isLoading}
isAllTime={isAllTime}
renderXLabel={renderDateLabels(unit, locale)}
/>
);
}
export default PageviewsChart;
export default PagepageviewsChart;

View file

@ -1,29 +1,9 @@
import { useMemo, useRef } from 'react';
import { format, startOfMinute, subMinutes, isBefore } from 'date-fns';
import { startOfMinute, subMinutes, isBefore } from 'date-fns';
import PageviewsChart from './PageviewsChart';
import { getDateArray } from 'lib/date';
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
import { RealtimeData } from 'lib/types';
function mapData(data: any[]) {
let last = 0;
const arr = [];
data?.reduce((obj, { timestamp }) => {
const t = startOfMinute(new Date(timestamp));
if (t.getTime() > last) {
obj = { x: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
arr.push(obj);
last = t.getTime();
} else {
obj.y += 1;
}
return obj;
}, {});
return arr;
}
export interface RealtimeChartProps {
data: RealtimeData;
unit: string;
@ -41,8 +21,8 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
}
return {
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
sessions: getDateArray(mapData(data.visitors), startDate, endDate, unit),
pageviews: data.series.views,
sessions: data.series.visitors,
};
}, [data, startDate, endDate, unit]);
@ -56,7 +36,14 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
}, [endDate]);
return (
<PageviewsChart {...props} unit={unit} data={chartData} animationDuration={animationDuration} />
<PageviewsChart
{...props}
minDate={startDate.toISOString()}
maxDate={endDate.toISOString()}
unit={unit}
data={chartData}
animationDuration={animationDuration}
/>
);
}

View file

@ -1,23 +1,21 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import Favicon from 'components/common/Favicon';
import { useMessages } from 'components/hooks';
import { Flexbox } from 'react-basics';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
export function ReferrersTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const renderLink = ({ x: referrer }) => {
return (
<Flexbox alignItems="center">
<FilterLink
id="referrer"
value={referrer}
externalUrl={`https://${referrer}`}
label={!referrer && formatMessage(labels.none)}
>
<Favicon domain={referrer} />
<FilterLink
id="referrer"
value={referrer}
externalUrl={`https://${referrer}`}
label={!referrer && formatMessage(labels.none)}
/>
</Flexbox>
</FilterLink>
);
};

View file

@ -1,28 +1,18 @@
import FilterLink from 'components/common/FilterLink';
import { emptyFilter } from 'lib/filters';
import { useLocale } from 'components/hooks';
import { useMessages } from 'components/hooks';
import { useCountryNames } from 'components/hooks';
import { useMessages, useLocale, useRegionNames } from 'components/hooks';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import regions from '../../../public/iso-3166-2.json';
import TypeIcon from 'components/common/TypeIcon';
export function RegionsTable(props: MetricsTableProps) {
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const countryNames = useCountryNames(locale);
const renderLabel = (code: string, country: string) => {
const region = code.includes('-') ? code : `${country}-${code}`;
return regions[region] ? `${regions[region]}, ${countryNames[country]}` : region;
};
const { getRegionName } = useRegionNames(locale);
const renderLink = ({ x: code, country }) => {
return (
<FilterLink id="region" className={locale} value={code} label={renderLabel(code, country)}>
<img
src={`${process.env.basePath}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
alt={code}
/>
<FilterLink id="region" value={code} label={getRegionName(code, country)}>
<TypeIcon type="country" value={country?.toLowerCase()} />
</FilterLink>
);
};

View file

@ -0,0 +1,30 @@
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { useMessages } from 'components/hooks';
import { Flexbox } from 'react-basics';
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}
/>
</>
);
}
export default TagsTable;

View file

@ -1,10 +1,10 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, HTMLAttributes } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import { colord } from 'colord';
import HoverTooltip from 'components/common/HoverTooltip';
import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants';
import { useTheme } from 'components/hooks';
import { useDateRange, useTheme, useWebsiteMetrics } from 'components/hooks';
import { useCountryNames } from 'components/hooks';
import { useLocale } from 'components/hooks';
import { useMessages } from 'components/hooks';
@ -12,16 +12,37 @@ import { formatLongNumber } from 'lib/format';
import { percentFilter } from 'lib/filters';
import styles from './WorldMap.module.css';
export function WorldMap({ data = [], className }: { data?: any[]; className?: string }) {
export function WorldMap({
websiteId,
data,
className,
...props
}: {
websiteId?: string;
data?: any[];
className?: string;
} & HTMLAttributes<HTMLDivElement>) {
const [tooltip, setTooltipPopup] = useState();
const { theme, colors } = useTheme();
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const countryNames = useCountryNames(locale);
const { countryNames } = useCountryNames(locale);
const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale);
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
const unknownLabel = formatMessage(labels.unknown);
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const { data: mapData } = useWebsiteMetrics(websiteId, {
type: 'country',
startAt: +startDate,
endAt: +endDate,
});
const metrics = useMemo(
() => (data || mapData ? percentFilter((data || mapData) as any[]) : []),
[data, mapData],
);
function getFillColor(code: string) {
const getFillColor = (code: string) => {
if (code === 'AQ') return;
const country = metrics?.find(({ x }) => x === code);
@ -32,29 +53,32 @@ export function WorldMap({ data = [], className }: { data?: any[]; className?: s
return colord(colors.map.baseColor)
[theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100))
.toHex();
}
};
function getOpacity(code) {
const getOpacity = (code: string) => {
return code === 'AQ' ? 0 : 1;
}
};
function handleHover(code) {
const handleHover = (code: string) => {
if (code === 'AQ') return;
const country = metrics?.find(({ x }) => x === code);
setTooltipPopup(
`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} ${visitorsLabel}` as any,
`${countryNames[code] || unknownLabel}: ${formatLongNumber(
country?.y || 0,
)} ${visitorsLabel}` as any,
);
}
};
return (
<div
{...props}
className={classNames(styles.container, className)}
data-tip=""
data-for="world-map-tooltip"
>
<ComposableMap projection="geoMercator">
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
<Geographies geography={`${process.env.basePath}${MAP_FILE}`}>
<Geographies geography={`${process.env.basePath || ''}${MAP_FILE}`}>
{({ geographies }) => {
return geographies.map(geo => {
const code = ISO_COUNTRIES[geo.id];