mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Merge branch 'dev' into hosts-support
This commit is contained in:
commit
d1559c3a98
281 changed files with 7555 additions and 1973 deletions
|
|
@ -26,7 +26,7 @@ export function BarChart(props: BarChartProps) {
|
|||
stacked = false,
|
||||
} = props;
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options: any = useMemo(() => {
|
||||
return {
|
||||
scales: {
|
||||
x: {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ export default function BarChartTooltip({ tooltip, unit }) {
|
|||
|
||||
return (
|
||||
<Flexbox direction="column" gap={10}>
|
||||
<div>{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||
<div>
|
||||
{formatDate(new Date(dataPoints[0].raw.d || dataPoints[0].raw.x), formats[unit], locale)}
|
||||
</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useRef, useEffect, useMemo, ReactNode } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import ChartJS, { LegendItem } from 'chart.js/auto';
|
||||
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
|
|
@ -17,7 +17,7 @@ export interface ChartProps {
|
|||
onUpdate?: (chart: any) => void;
|
||||
onTooltip?: (model: any) => void;
|
||||
className?: string;
|
||||
chartOptions?: { [key: string]: any };
|
||||
chartOptions?: ChartOptions;
|
||||
tooltip?: ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -79,24 +79,28 @@ export function Chart({
|
|||
};
|
||||
|
||||
const updateChart = (data: any) => {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
if (data.datasets.length === chart.current.data.datasets.length) {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
chart.current.data.datasets = data.datasets;
|
||||
}
|
||||
|
||||
chart.current.options = options;
|
||||
|
||||
// Allow config changes before update
|
||||
onUpdate?.(chart.current);
|
||||
|
||||
setLegendItems(chart.current.legend.legendItems);
|
||||
|
||||
chart.current.update(updateMode);
|
||||
|
||||
setLegendItems(chart.current.legend.legendItems);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,8 @@
|
|||
.table {
|
||||
grid-template-rows: repeat(auto-fit, max-content);
|
||||
}
|
||||
|
||||
.table td {
|
||||
align-items: center;
|
||||
max-height: max-content;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 300px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { Banner, Loading, SearchField } from 'react-basics';
|
|||
import { useMessages } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import Pager from 'components/common/Pager';
|
||||
import styles from './DataTable.module.css';
|
||||
import { FilterQueryResult } from 'lib/types';
|
||||
import styles from './DataTable.module.css';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.favicon {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import styles from './Favicon.module.css';
|
||||
|
||||
function getHostName(url: string) {
|
||||
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im);
|
||||
return match && match.length > 1 ? match[1] : null;
|
||||
|
|
@ -14,7 +12,6 @@ export function Favicon({ domain, ...props }) {
|
|||
|
||||
return hostName ? (
|
||||
<img
|
||||
className={styles.favicon}
|
||||
src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`}
|
||||
width={16}
|
||||
height={16}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, useRef } from 'react';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { useApi } from 'components/hooks';
|
||||
import { useApi } from './useApi';
|
||||
import { REALTIME_INTERVAL, REALTIME_RANGE } from 'lib/constants';
|
||||
import { startOfMinute, subMinutes } from 'date-fns';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { useFilterParams } from '../useFilterParams';
|
|||
|
||||
export function useWebsiteMetrics(
|
||||
websiteId: string,
|
||||
type: string,
|
||||
limit: number,
|
||||
queryParams: { type: string; limit: number; search: string; startAt?: number; endAt?: number },
|
||||
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
@ -17,19 +16,17 @@ export function useWebsiteMetrics(
|
|||
{
|
||||
websiteId,
|
||||
...params,
|
||||
type,
|
||||
limit,
|
||||
...queryParams,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const filters = { ...params };
|
||||
|
||||
filters[type] = undefined;
|
||||
filters[queryParams.type] = undefined;
|
||||
|
||||
const data = await get(`/websites/${websiteId}/metrics`, {
|
||||
...filters,
|
||||
type,
|
||||
limit,
|
||||
...queryParams,
|
||||
});
|
||||
|
||||
options?.onDataLoad?.(data);
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import { useFilterParams } from '..//useFilterParams';
|
|||
|
||||
export function useWebsitePageviews(
|
||||
websiteId: string,
|
||||
compare?: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:pageviews', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
|
||||
queryKey: ['websites:pageviews', { websiteId, ...params, compare }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, { ...params, compare }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
|
||||
export function useWebsiteStats(
|
||||
websiteId: string,
|
||||
compare?: string,
|
||||
options?: { [key: string]: string },
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:stats', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, params),
|
||||
queryKey: ['websites:stats', { websiteId, ...params, compare }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from 'components/hooks';
|
||||
import { useApi } from './useApi';
|
||||
|
||||
export function useWebsiteValues({
|
||||
websiteId,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export function useCountryNames(locale: string) {
|
|||
const [list, setList] = useState(countryNames[locale] || enUS);
|
||||
|
||||
async function loadData(locale: string) {
|
||||
const { data } = await httpGet(`${process.env.basePath}/intl/country/${locale}.json`);
|
||||
const { data } = await httpGet(`${process.env.basePath || ''}/intl/country/${locale}.json`);
|
||||
|
||||
if (data) {
|
||||
countryNames[locale] = data;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
import { getMinimumUnit, parseDateRange } from 'lib/date';
|
||||
import { setItem } from 'next-basics';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites';
|
||||
import appStore, { setDateRange } from 'store/app';
|
||||
import { DateRange } from 'lib/types';
|
||||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './queries/useApi';
|
||||
|
||||
export function useDateRange(websiteId?: string): [DateRange, (value: string | DateRange) => void] {
|
||||
export function useDateRange(websiteId?: string): {
|
||||
dateRange: DateRange;
|
||||
saveDateRange: (value: string | DateRange) => void;
|
||||
dateCompare: string;
|
||||
saveDateCompare: (value: string) => void;
|
||||
} {
|
||||
const { get } = useApi();
|
||||
const { locale } = useLocale();
|
||||
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
||||
const defaultConfig = DEFAULT_DATE_RANGE;
|
||||
const globalConfig = appStore(state => state.dateRange);
|
||||
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
|
||||
const dateCompare = websiteStore(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
|
||||
|
||||
const saveDateRange = async (value: DateRange | string) => {
|
||||
if (websiteId) {
|
||||
|
|
@ -45,7 +51,11 @@ export function useDateRange(websiteId?: string): [DateRange, (value: string | D
|
|||
}
|
||||
};
|
||||
|
||||
return [dateRange, saveDateRange];
|
||||
const saveDateCompare = (value: string) => {
|
||||
setWebsiteDateCompare(websiteId, value);
|
||||
};
|
||||
|
||||
return { dateRange, saveDateRange, dateCompare, saveDateCompare };
|
||||
}
|
||||
|
||||
export default useDateRange;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { useTimezone } from './useTimezone';
|
|||
import { zonedTimeToUtc } from 'date-fns-tz';
|
||||
|
||||
export function useFilterParams(websiteId: string) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, offset } = dateRange;
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit } = dateRange;
|
||||
const { timezone } = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, title, query, os, browser, device, country, region, city, event },
|
||||
|
|
@ -15,7 +15,6 @@ export function useFilterParams(websiteId: string) {
|
|||
startAt: +zonedTimeToUtc(startDate, timezone),
|
||||
endAt: +zonedTimeToUtc(endDate, timezone),
|
||||
unit,
|
||||
offset,
|
||||
timezone,
|
||||
url,
|
||||
referrer,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export function useLanguageNames(locale) {
|
|||
const [list, setList] = useState(languageNames[locale] || enUS);
|
||||
|
||||
async function loadData(locale) {
|
||||
const { data } = await httpGet(`${process.env.basePath}/intl/language/${locale}.json`);
|
||||
const { data } = await httpGet(`${process.env.basePath || ''}/intl/language/${locale}.json`);
|
||||
|
||||
if (data) {
|
||||
languageNames[locale] = data;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export function useLocale() {
|
|||
const dateLocale = getDateLocale(locale);
|
||||
|
||||
async function loadMessages(locale: string) {
|
||||
const { ok, data } = await httpGet(`${process.env.basePath}/intl/messages/${locale}.json`);
|
||||
const { ok, data } = await httpGet(
|
||||
`${process.env.basePath || ''}/intl/messages/${locale}.json`,
|
||||
);
|
||||
|
||||
if (ok) {
|
||||
messages[locale] = data;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Bolt from 'assets/bolt.svg';
|
|||
import Calendar from 'assets/calendar.svg';
|
||||
import Change from 'assets/change.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
import Compare from 'assets/compare.svg';
|
||||
import Dashboard from 'assets/dashboard.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import Gear from 'assets/gear.svg';
|
||||
|
|
@ -32,6 +33,7 @@ const icons = {
|
|||
Calendar,
|
||||
Change,
|
||||
Clock,
|
||||
Compare,
|
||||
Dashboard,
|
||||
Eye,
|
||||
Gear,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function RefreshButton({
|
|||
isLoading?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
|
||||
function handleClick() {
|
||||
if (!isLoading && dateRange) {
|
||||
|
|
|
|||
|
|
@ -6,23 +6,38 @@ import DateFilter from './DateFilter';
|
|||
import styles from './WebsiteDateFilter.module.css';
|
||||
import { DateRange } from 'lib/types';
|
||||
|
||||
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
||||
export function WebsiteDateFilter({
|
||||
websiteId,
|
||||
showAllTime = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showAllTime?: boolean;
|
||||
}) {
|
||||
const { dir } = useLocale();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { dateRange, saveDateRange } = useDateRange(websiteId);
|
||||
const { value, startDate, endDate, offset } = dateRange;
|
||||
const disableForward =
|
||||
value === 'all' || isAfter(getOffsetDateRange(dateRange, 1).startDate, new Date());
|
||||
|
||||
const handleChange = (value: string | DateRange) => {
|
||||
setDateRange(value);
|
||||
saveDateRange(value);
|
||||
};
|
||||
|
||||
const handleIncrement = (increment: number) => {
|
||||
setDateRange(getOffsetDateRange(dateRange, increment));
|
||||
saveDateRange(getOffsetDateRange(dateRange, increment));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
offset={offset}
|
||||
onChange={handleChange}
|
||||
showAllTime={showAllTime}
|
||||
/>
|
||||
{value !== 'all' && !value.startsWith('range') && (
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={() => handleIncrement(-1)}>
|
||||
|
|
@ -37,15 +52,6 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
offset={offset}
|
||||
onChange={handleChange}
|
||||
showAllTime={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.row.compare {
|
||||
grid-template-columns: max-content 1fr 1fr;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding: 20px;
|
||||
min-height: 430px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { CSSProperties } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { mapChildren } from 'react-basics';
|
||||
// eslint-disable-next-line css-modules/no-unused-class
|
||||
import styles from './Grid.module.css';
|
||||
|
||||
export interface GridProps {
|
||||
|
|
@ -19,13 +20,13 @@ export function Grid({ className, style, children }: GridProps) {
|
|||
|
||||
export function GridRow(props: {
|
||||
[x: string]: any;
|
||||
columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one';
|
||||
columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one' | 'compare';
|
||||
className?: string;
|
||||
children?: any;
|
||||
}) {
|
||||
const { columns = 'two', className, children, ...otherProps } = props;
|
||||
return (
|
||||
<div {...otherProps} className={classNames(styles.row, className)}>
|
||||
<div {...otherProps} className={classNames(styles.row, className, { [styles[columns]]: true })}>
|
||||
{mapChildren(children, child => {
|
||||
return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ export const labels = defineMessages({
|
|||
leaveTeam: { id: 'label.leave-team', defaultMessage: 'Leave team' },
|
||||
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
||||
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
||||
entry: { id: 'label.entry', defaultMessage: 'Entry URL' },
|
||||
exit: { id: 'label.exit', defaultMessage: 'Exit URL' },
|
||||
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
||||
hosts: { id: 'label.hosts', defaultMessage: 'Hosts' },
|
||||
screens: { id: 'label.screens', defaultMessage: 'Screens' },
|
||||
|
|
@ -96,6 +98,9 @@ export const labels = defineMessages({
|
|||
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
||||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
||||
count: { id: 'label.count', defaultMessage: 'Count' },
|
||||
average: { id: 'label.average', defaultMessage: 'Average' },
|
||||
sum: { id: 'label.sum', defaultMessage: 'Sum' },
|
||||
event: { id: 'label.event', defaultMessage: 'Event' },
|
||||
events: { id: 'label.events', defaultMessage: 'Events' },
|
||||
query: { id: 'label.query', defaultMessage: 'Query' },
|
||||
|
|
@ -108,6 +113,7 @@ export const labels = defineMessages({
|
|||
views: { id: 'label.views', defaultMessage: 'Views' },
|
||||
none: { id: 'label.none', defaultMessage: 'None' },
|
||||
clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' },
|
||||
property: { id: 'label.property', defaultMessage: 'Property' },
|
||||
today: { id: 'label.today', defaultMessage: 'Today' },
|
||||
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
||||
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
||||
|
|
@ -131,7 +137,7 @@ export const labels = defineMessages({
|
|||
uniqueVisitors: { id: 'label.unique-visitors', defaultMessage: 'Unique visitors' },
|
||||
bounceRate: { id: 'label.bounce-rate', defaultMessage: 'Bounce rate' },
|
||||
viewsPerVisit: { id: 'label.views-per-visit', defaultMessage: 'Views per visit' },
|
||||
averageVisitTime: { id: 'label.average-visit-time', defaultMessage: 'Average visit time' },
|
||||
visitDuration: { id: 'label.visit-duration', defaultMessage: 'Visit duration' },
|
||||
desktop: { id: 'label.desktop', defaultMessage: 'Desktop' },
|
||||
laptop: { id: 'label.laptop', defaultMessage: 'Laptop' },
|
||||
tablet: { id: 'label.tablet', defaultMessage: 'Tablet' },
|
||||
|
|
@ -179,8 +185,6 @@ export const labels = defineMessages({
|
|||
before: { id: 'label.before', defaultMessage: 'Before' },
|
||||
after: { id: 'label.after', defaultMessage: 'After' },
|
||||
total: { id: 'label.total', defaultMessage: 'Total' },
|
||||
sum: { id: 'label.sum', defaultMessage: 'Sum' },
|
||||
average: { id: 'label.average', defaultMessage: 'Average' },
|
||||
min: { id: 'label.min', defaultMessage: 'Min' },
|
||||
max: { id: 'label.max', defaultMessage: 'Max' },
|
||||
unique: { id: 'label.unique', defaultMessage: 'Unique' },
|
||||
|
|
@ -222,6 +226,10 @@ export const labels = defineMessages({
|
|||
id: 'message.viewed-page',
|
||||
defaultMessage: 'Viewed page',
|
||||
},
|
||||
collectedData: {
|
||||
id: 'message.collected-data',
|
||||
defaultMessage: 'Collected data',
|
||||
},
|
||||
triggeredEvent: {
|
||||
id: 'message.triggered-event',
|
||||
defaultMessage: 'Triggered event',
|
||||
|
|
@ -236,7 +244,25 @@ export const labels = defineMessages({
|
|||
defaultMessage: 'Track your campaigns through UTM parameters.',
|
||||
},
|
||||
steps: { id: 'label.steps', defaultMessage: 'Steps' },
|
||||
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
|
||||
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
|
||||
addStep: { id: 'label.add-step', defaultMessage: 'Add step' },
|
||||
goal: { id: 'label.goal', defaultMessage: 'Goal' },
|
||||
goals: { id: 'label.goals', defaultMessage: 'Goals' },
|
||||
goalsDescription: {
|
||||
id: 'label.goals-description',
|
||||
defaultMessage: 'Track your goals for pageviews and events.',
|
||||
},
|
||||
journey: { id: 'label.journey', defaultMessage: 'Journey' },
|
||||
journeyDescription: {
|
||||
id: 'label.journey-description',
|
||||
defaultMessage: 'Understand how users nagivate through your website.',
|
||||
},
|
||||
compare: { id: 'label.compare', defaultMessage: 'Compare' },
|
||||
current: { id: 'label.current', defaultMessage: 'Current' },
|
||||
previous: { id: 'label.previous', defaultMessage: 'Previous' },
|
||||
previousPeriod: { id: 'label.previous-period', defaultMessage: 'Previous period' },
|
||||
previousYear: { id: 'label.previous-year', defaultMessage: 'Previous year' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function BrowsersTable(props: MetricsTableProps) {
|
|||
return (
|
||||
<FilterLink id="browser" value={browser} label={formatBrowser(browser)}>
|
||||
<img
|
||||
src={`${process.env.basePath}/images/browsers/${browser || 'unknown'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/browsers/${browser || 'unknown'}.png`}
|
||||
alt={browser}
|
||||
width={16}
|
||||
height={16}
|
||||
|
|
|
|||
26
src/components/metrics/ChangeLabel.module.css
Normal file
26
src/components/metrics/ChangeLabel.module.css
Normal 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);
|
||||
}
|
||||
46
src/components/metrics/ChangeLabel.tsx
Normal file
46
src/components/metrics/ChangeLabel.tsx
Normal 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;
|
||||
|
|
@ -20,7 +20,7 @@ export function CitiesTable(props: MetricsTableProps) {
|
|||
<FilterLink id="city" value={city} label={renderLabel(city, country)}>
|
||||
{country && (
|
||||
<img
|
||||
src={`${process.env.basePath}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
alt={country}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function CountriesTable({
|
|||
label={formatCountry(code)}
|
||||
>
|
||||
<img
|
||||
src={`${process.env.basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
</FilterLink>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ export function DevicesTable(props: MetricsTableProps) {
|
|||
return (
|
||||
<FilterLink id="device" value={labels[device] && device} label={formatDevice(device)}>
|
||||
<img
|
||||
src={`${process.env.basePath}/images/device/${device?.toLowerCase() || 'unknown'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/device/${
|
||||
device?.toLowerCase() || 'unknown'
|
||||
}.png`}
|
||||
alt={device}
|
||||
width={16}
|
||||
height={16}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export interface EventsChartProps {
|
|||
}
|
||||
|
||||
export function EventsChart({ websiteId, className }: EventsChartProps) {
|
||||
const [{ startDate, endDate, unit }] = useDateRange(websiteId);
|
||||
const {
|
||||
dateRange: { startDate, endDate, unit },
|
||||
} = useDateRange(websiteId);
|
||||
const { locale } = useLocale();
|
||||
const { data, isLoading } = useWebsiteEvents(websiteId);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?: typeof formatNumber;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,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;
|
||||
|
|
@ -27,6 +26,8 @@ export interface MetricsTableProps extends ListTableProps {
|
|||
onDataLoad?: (data: any) => void;
|
||||
onSearch?: (search: string) => void;
|
||||
allowSearch?: boolean;
|
||||
showMore?: boolean;
|
||||
params?: { [key: string]: any };
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +40,8 @@ export function MetricsTable({
|
|||
onDataLoad,
|
||||
delay = null,
|
||||
allowSearch = false,
|
||||
showMore = true,
|
||||
params,
|
||||
children,
|
||||
...props
|
||||
}: MetricsTableProps) {
|
||||
|
|
@ -48,10 +51,14 @@ export function MetricsTable({
|
|||
const { formatMessage, labels } = useMessages();
|
||||
const { dir } = useLocale();
|
||||
|
||||
const { data, isLoading, isFetched, error } = useWebsiteMetrics(websiteId, type, limit, {
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
onDataLoad,
|
||||
});
|
||||
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
|
||||
websiteId,
|
||||
{ type, limit, search, ...params },
|
||||
{
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
onDataLoad,
|
||||
},
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
|
|
@ -94,7 +101,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}>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
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 = [
|
||||
|
|
@ -26,6 +28,14 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
|
|||
label: 'URL',
|
||||
key: 'url',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.entry),
|
||||
key: 'entry',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.exit),
|
||||
key: 'exit',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.title),
|
||||
key: 'title',
|
||||
|
|
@ -35,12 +45,12 @@ 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={
|
||||
view === 'url'
|
||||
? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`
|
||||
view !== 'title'
|
||||
? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
|
|
@ -50,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)}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import { renderDateLabels } from 'lib/charts';
|
|||
|
||||
export interface PageviewsChartProps extends BarChartProps {
|
||||
data: {
|
||||
sessions: any[];
|
||||
pageviews: any[];
|
||||
sessions: any[];
|
||||
compare?: {
|
||||
pageviews: any[];
|
||||
sessions: any[];
|
||||
};
|
||||
};
|
||||
unit: string;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -29,13 +33,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]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function RegionsTable(props: MetricsTableProps) {
|
|||
return (
|
||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code, country)}>
|
||||
<img
|
||||
src={`${process.env.basePath}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
</FilterLink>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function WorldMap({ data = [], className }: { data?: any[]; className?: s
|
|||
>
|
||||
<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];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue