Merge branch 'dev' into hosts-support

This commit is contained in:
Mike Cao 2024-06-18 23:02:14 -07:00 committed by GitHub
commit d1559c3a98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
281 changed files with 7555 additions and 1973 deletions

View file

@ -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}

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

@ -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}
/>
)}

View file

@ -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>

View file

@ -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}

View file

@ -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);

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

@ -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?: 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>
);
};

View file

@ -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}>

View file

@ -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)}

View file

@ -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]);

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

@ -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>

View file

@ -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];